# LLM cost tracking (AI Foundry and in-house) Below is a command-line tool to fetch the costs of LLM (Large Language Model) deployments in Azure AI Foundry for a set of subscriptions within your directory. The tool generates reports that document the total costs, per subscription, and per resource (group). Subscriptions can be specified in a configuration file or discovered automatically. In addition, you can specify resources that you want to track as costs in addition to AI Foundry deployments, such as an AKS cluster, the container registry, or the API Management instances you are using are for running your own, private LLM deployments. Finally, it can show plots of daily cost graphs using Pandas and matplotlib. It simplifies cost tracking and provides immediate insights into the (amortized) expenses associated with any Azure-based LLM services you are using. ## Prerequisites - Python (3.11 or higher); dependencies: `pip install requests pyyaml pandas matplotlib types-pyyaml types-requests pandas-stubs` - Azure account with permissions to access subscription cost data - Azure Cost Management API enabled for your subscriptions - Azure CLI `az` installed (for token generation) ## Setup Instructions ### Optional: Set Azure Access Token By default, the script will fetch the access token on its own using the `az` CLI tool. To generate an Azure Access Token using the Azure CLI: ```bash az account get-access-token --resource https://management.azure.com ``` Copy the `accessToken` value from the output. Set the Access Token as an Environment Variable: - On macOS/Linux: ```bash export AZURE_ACCESS_TOKEN=<your-access-token> ``` - On Windows (Command Prompt): ```cmd set AZURE_ACCESS_TOKEN=<your-access-token> ``` - On Windows (PowerShell): ```powershell $env:AZURE_ACCESS_TOKEN="<your-access-token>" ``` To make this easy, create a script `set_azure_token.sh` with this content: ```shell #!/bin/bash # Get the access token using Azure CLI TOKEN=$(az account get-access-token --resource https://management.azure.com --query accessToken -o tsv) if [ -z "$TOKEN" ]; then echo "Error: Failed to retrieve access token. Make sure you are logged in with 'az login'." exit 1 fi export AZURE_ACCESS_TOKEN="$TOKEN" ``` And then set the token using `source set_azure_token.sh` ### Optional: Configure Subscriptions Update the `config.yaml` file with your Azure subscription details. The file includes a list of subscriptions to track: ```yaml subscriptions: - id: $UUID... name: $NAME... resource_groups: - $NAME - ... - id: ... ``` Add or modify subscription entries as needed for your environment. To use those subscriptions, use the command-line flag `--configure` ## Using the tool ```bash python track_costs.py --help ``` To capture the full cost report in a markdown file: ```bash python track_costs.py > report.md ``` # Code ```python #!/usr/bin/env python3 import os import sys import logging import requests import argparse import time import subprocess import re from datetime import datetime, timedelta import pandas as pd import matplotlib.pyplot as plt import matplotlib.dates as mdates from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry logging.basicConfig( level=logging.INFO, format="%(levelname)s: %(message)s", stream=sys.stderr ) AZURE_MANAGEMENT_API = "https://management.azure.com" AZURE_GRAPH_API = "https://graph.microsoft.com" API_VERSION_SUBSCRIPTIONS = "2022-12-01" API_VERSION_COST_MANAGEMENT = "2023-03-01" API_VERSION_COGNITIVE_SERVICES = "2023-05-01" RATE_LIMIT_DELAY_SECONDS = 2.0 SUBSCRIPTION_DELAY_SECONDS = 3.0 VALID_PRESET_TIMEFRAMES = ["this-week", "7days", "month-to-date", "last-month", "30days"] _principal_name_cache: dict[str, str | None] = {} def create_session_with_retries(): """Create a requests session with retry logic for rate limiting (429) and server errors.""" session = requests.Session() retry_strategy = Retry( total=5, backoff_factor=2, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET", "POST"], ) adapter = HTTPAdapter(max_retries=retry_strategy) session.mount("http://", adapter) session.mount("https://", adapter) return session def get_azure_token(resource=f"{AZURE_MANAGEMENT_API}/"): """Get Azure access token from environment variable or Azure CLI. Args: resource: The Azure resource URL to get a token for. Defaults to Azure Management API. Returns: Access token string, or None if unable to retrieve. """ env_var = ( "AZURE_ACCESS_TOKEN" if resource == f"{AZURE_MANAGEMENT_API}/" else "AZURE_GRAPH_TOKEN" ) token = os.getenv(env_var) if token: logging.debug(f"Using token from environment variable {env_var}") return token logging.debug(f"az account get-access-token --resource {resource} --query accessToken -o tsv") try: result = subprocess.run( [ "az", "account", "get-access-token", "--resource", resource, "--query", "accessToken", "-o", "tsv", ], capture_output=True, text=True, check=True, ) return result.stdout.strip() except subprocess.CalledProcessError as e: logging.debug(f"Azure CLI error: {e.stderr if hasattr(e, 'stderr') else str(e)}") return None except FileNotFoundError: logging.error("Azure CLI not found") return None def load_subscriptions(config_file): """Load subscriptions from the configuration file.""" try: import yaml with open(config_file, "r") as file: return yaml.safe_load(file).get("subscriptions", []) except ImportError: logging.error("Error: 'pyyaml' module is not installed.") logging.info("Install pyyaml using: pip install pyyaml") sys.exit(1) def generate_timeline_plots(daily_data): """Generate timeline plots from daily cost data.""" if not daily_data: logging.info("No daily data to plot") return df = pd.DataFrame(daily_data) df["date"] = pd.to_datetime(df["date"].astype(str), format="%Y%m%d") pivot_df = df.pivot_table( index="date", columns="resource_name", values="cost", aggfunc="sum", fill_value=0, ) if pivot_df.empty: logging.info("No data available for plotting") return plt.figure(figsize=(12, 6)) for resource in pivot_df.columns: plt.plot(pivot_df.index, pivot_df[resource], marker="o", label=resource) plt.xlabel("Date") plt.ylabel("Cost ($)") plt.title("Daily Costs by Resource") plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") plt.grid(True, alpha=0.3) plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d")) plt.gcf().autofmt_xdate() plt.tight_layout() plt.savefig("resource_group_costs.png", dpi=300, bbox_inches="tight") plt.close() logging.info("Saved resource_group_costs.png") cumulative_df = pivot_df.cumsum() plt.figure(figsize=(12, 6)) plt.stackplot( cumulative_df.index, *[cumulative_df[col].values for col in cumulative_df.columns], labels=list(cumulative_df.columns), alpha=0.8, ) plt.xlabel("Date") plt.ylabel("Cumulative Cost ($)") plt.title("Cumulative Total Costs (Stacked)") plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") plt.grid(True, alpha=0.3) plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d")) plt.gcf().autofmt_xdate() plt.tight_layout() plt.savefig("total_costs.png", dpi=300, bbox_inches="tight") plt.close() logging.info("Saved total_costs.png") def discover_subscriptions(access_token): """Discover all Azure subscriptions the current user has access to. Args: access_token: Azure Management API access token. Returns: List of dicts with 'id' and 'name' keys for each subscription. """ url = f"{AZURE_MANAGEMENT_API}/subscriptions?api-version={API_VERSION_SUBSCRIPTIONS}" headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", } try: session = create_session_with_retries() response = session.get(url, headers=headers) response.raise_for_status() data = response.json() logging.debug(f"Found {len(data.get('value', []))} subscriptions") subscriptions = [] for sub in data.get("value", []): subscriptions.append( {"id": sub.get("subscriptionId"), "name": sub.get("displayName")} ) return subscriptions except requests.exceptions.RequestException as e: logging.error(f"Error discovering subscriptions: {e}") if hasattr(e, 'response') and e.response is not None: logging.debug(f"Response text: {e.response.text[:500]}") return [] def get_date_range(timeframe): """Calculate start and end dates for a given timeframe. Supports formats: - 'this-week': Current week - '7days': Last 7 days - 'month-to-date': Current month to today - 'last-month': Previous complete month - '30days': Last 30 days - 'mon-2025', 'january-2026': Specific month (abbreviated or full month name) """ today = datetime.now() # Try to parse month-year format (e.g., "nov-2025", "november-2025") month_year_pattern = r'^([a-z]+)-([0-9]{4}) match = re.match(month_year_pattern, timeframe.lower()) if match: month_str, year_str = match.groups() try: # Try full month name first month = datetime.strptime(month_str, '%B').month except ValueError: try: # Try abbreviated month name month = datetime.strptime(month_str, '%b').month except ValueError: return None, None try: year = int(year_str) # First day of the month start_date = datetime(year, month, 1) # Last day of the month if month == 12: end_date = datetime(year + 1, 1, 1) - timedelta(days=1) else: end_date = datetime(year, month + 1, 1) - timedelta(days=1) return start_date, end_date except ValueError: return None, None if timeframe == "this-week": start_of_week = today - timedelta(days=today.weekday()) return start_of_week.replace(hour=0, minute=0, second=0, microsecond=0), today elif timeframe == "7days": end_date = today - timedelta(days=1) start_date = end_date - timedelta(days=6) return start_date, end_date elif timeframe == "month-to-date": return today.replace(day=1, hour=0, minute=0, second=0, microsecond=0), today elif timeframe == "last-month": first_day_current_month = today.replace(day=1) last_day_previous_month = first_day_current_month - timedelta(days=1) return last_day_previous_month.replace(day=1), last_day_previous_month elif timeframe == "30days": end_date = today - timedelta(days=1) start_date = end_date - timedelta(days=29) return start_date, end_date return None, None def discover_ai_foundry_resources(subscription_id, access_token): """Discover Azure AI Foundry resources with model deployments in a subscription. This separate discovery step is necessary because the Cost Management API only returns resources that have incurred costs. Resources with deployments but zero usage in the queried timeframe won't appear in cost data. Currently discovers: - Microsoft.CognitiveServices/accounts (OpenAI, AIServices) Args: subscription_id: Azure subscription ID. access_token: Azure Management API access token. Returns: List of dicts with 'id', 'name', and 'type' keys for each resource. """ headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", } discovered_resources = [] session = create_session_with_retries() cognitive_url = ( f"{AZURE_MANAGEMENT_API}/subscriptions/{subscription_id}" f"/providers/Microsoft.CognitiveServices/accounts" f"?api-version={API_VERSION_COGNITIVE_SERVICES}" ) try: response = session.get(cognitive_url, headers=headers) response.raise_for_status() data = response.json() for resource in data.get("value", []): kind = resource.get("kind", "") resource_name = resource.get("name", "Unknown").lower() logging.debug(f"Found {kind} resource: {resource_name}") if kind in ["OpenAI", "AIServices"]: discovered_resources.append({ "id": resource.get("id", ""), "name": resource_name, "type": "CognitiveServices" }) except requests.exceptions.RequestException as e: logging.error(f"Error discovering Cognitive Services resources: {e}") if hasattr(e, 'response') and e.response is not None: logging.debug(f"Response text: {e.response.text[:500]}") # Future: Add discovery for other AI Foundry resource types here # Example: Microsoft.MachineLearningServices/workspaces return discovered_resources def resolve_principal_name(principal_id, access_token): """Resolve a service principal or application UUID to a human-readable name. Args: principal_id: The UUID of the service principal or application. access_token: Azure access token (used for logging context only). Returns: Display name with object type (e.g., "My App (servicePrincipal)"), or the original principal_id if resolution fails, or None if the principal was deleted. """ if not principal_id or "@" in principal_id: return principal_id if principal_id in _principal_name_cache: return _principal_name_cache[principal_id] graph_token = get_azure_token(f"{AZURE_GRAPH_API}/") if not graph_token: logging.info(f"Failed to get Graph token for resolving {principal_id}") _principal_name_cache[principal_id] = principal_id return principal_id headers = { "Authorization": f"Bearer {graph_token}", "Content-Type": "application/json", } try: session = create_session_with_retries() dir_obj_url = f"{AZURE_GRAPH_API}/v1.0/directoryObjects/{principal_id}" logging.debug(f"curl -H 'Authorization: Bearer $AZURE_GRAPH_TOKEN' '{dir_obj_url}'") response = session.get(dir_obj_url, headers=headers) logging.debug(f"Response status: {response.status_code}") if response.status_code == 200: data = response.json() display_name = data.get("displayName") obj_type = data.get("@odata.type", "").split(".")[-1] if display_name: if obj_type: _principal_name_cache[principal_id] = f"{display_name} ({obj_type})" else: _principal_name_cache[principal_id] = display_name return _principal_name_cache[principal_id] elif response.status_code == 404: _principal_name_cache[principal_id] = None return None else: logging.info(f"Failed to resolve {principal_id}: {response.status_code}") except requests.exceptions.RequestException as e: logging.info(f"Error resolving {principal_id}: {e}") _principal_name_cache[principal_id] = principal_id return principal_id def fetch_openai_deployments(resource_id, access_token): """Fetch model deployments for an Azure OpenAI or AI Services resource. Args: resource_id: Full Azure resource ID. access_token: Azure Management API access token. Returns: List of deployment dicts with 'name', 'model', 'createdBy', 'createdAt'. """ url = ( f"{AZURE_MANAGEMENT_API}{resource_id}/deployments" f"?api-version={API_VERSION_COGNITIVE_SERVICES}" ) headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", } try: session = create_session_with_retries() response = session.get(url, headers=headers) response.raise_for_status() data = response.json() logging.debug(f"Found {len(data.get('value', []))} deployments") deployments = [] for deployment in data.get("value", []): deployment_name = deployment.get("name", "Unknown") model_name = ( deployment.get("properties", {}).get("model", {}).get("name", "Unknown") ) system_data = deployment.get("systemData", {}) created_by = system_data.get("createdBy", "Unknown") created_at = system_data.get("createdAt", "Unknown") logging.debug(f"Processing deployment {deployment_name} (model: {model_name})") created_by = resolve_principal_name(created_by, access_token) deployments.append({ "name": deployment_name, "model": model_name, "createdBy": created_by, "createdAt": created_at }) return deployments except requests.exceptions.RequestException as e: logging.info(f"Error fetching deployments: {e}") return [] def _build_timeframe_config(timeframe): """Build the timeframe configuration for Cost Management API. Args: timeframe: One of the preset timeframes or a month-year format (e.g., 'nov-2025'). Returns: Dict with 'timeframe' key and optionally 'timePeriod' for custom ranges. """ if timeframe == "this-week": return {"timeframe": "WeekToDate"} elif timeframe == "month-to-date": return {"timeframe": "MonthToDate"} else: start_date, end_date = get_date_range(timeframe) if start_date and end_date: return { "timeframe": "Custom", "timePeriod": { "from": start_date.strftime("%Y-%m-%dT00:00:00Z"), "to": end_date.strftime("%Y-%m-%dT23:59:59Z"), }, } return {"timeframe": "MonthToDate"} def _extract_resource_name(resource_id): """Extract the resource name from an Azure resource ID. Args: resource_id: Full Azure resource ID path. Returns: Lowercase resource name, or 'unknown' if extraction fails. """ if not resource_id: return "unknown" parts = resource_id.split("/") return parts[-1].lower() if parts else "unknown" def fetch_llm_costs( subscription_id, subscription_name, access_token, timeframe="month-to-date", daily=False, resource_groups=None, ): """Fetch amortized costs of LLM deployments for a subscription. Amortized costs show the true cost of resources in the period, including reserved instance amortization and commitment-based pricing. Args: subscription_id: Azure subscription ID. subscription_name: Display name for the subscription. access_token: Azure Management API access token. timeframe: Time period for cost analysis. daily: If True, return daily granularity for timeline plots. resource_groups: Optional list of resource group names to track. Returns: Dict with 'subscription', 'resources', and 'daily_data' keys. """ if resource_groups is None: resource_groups = [] url = ( f"{AZURE_MANAGEMENT_API}/subscriptions/{subscription_id}" f"/providers/Microsoft.CostManagement/query" f"?api-version={API_VERSION_COST_MANAGEMENT}" ) headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", } timeframe_config = _build_timeframe_config(timeframe) payload = { "type": "AmortizedCost", **timeframe_config, "dataset": { "granularity": "Daily" if daily else "None", "aggregation": {"totalCost": {"name": "Cost", "function": "Sum"}}, "grouping": [ {"type": "Dimension", "name": "ServiceName"}, {"type": "Dimension", "name": "ResourceId"}, {"type": "Dimension", "name": "ResourceGroup"}, ], }, } resources = {} daily_data = [] try: session = create_session_with_retries() ai_resources = discover_ai_foundry_resources(subscription_id, access_token) for ai_resource in ai_resources: resource_id = ai_resource["id"] resource_name = ai_resource["name"] deployments = fetch_openai_deployments(resource_id, access_token) resources[resource_name] = { "cost": 0.0, "deployments": deployments, "resource_id": resource_id, "is_foundry": True, } response = session.post(url, headers=headers, json=payload) response.raise_for_status() data = response.json() time.sleep(RATE_LIMIT_DELAY_SECONDS) rows = data.get("properties", {}).get("rows", []) logging.debug(f"Cost API returned {len(rows)} rows") if resource_groups and logging.getLogger().level == logging.DEBUG: logging.debug("Sample rows for debugging (first 5):") for idx, row in enumerate(rows[:5]): logging.debug(f" Row {idx}: {row}") total_from_api = sum(row[0] for row in rows) logging.debug(f"Total cost from API: ${total_from_api:.2f}") resource_groups_seen = set() for row in rows: if daily: cost = row[0] date = row[1] if len(row) > 1 else None service_name = row[2] if len(row) > 2 else "Unknown" resource_id = row[3] if len(row) > 3 else "" resource_group = row[4] if len(row) > 4 else "" else: cost = row[0] date = None service_name = row[1] if len(row) > 1 else "Unknown" resource_id = row[2] if len(row) > 2 else "" resource_group = row[3] if len(row) > 3 else "" if resource_group: resource_groups_seen.add(resource_group.lower()) is_foundry_cost = "Foundry Models" in service_name if is_foundry_cost: resource_name = _extract_resource_name(resource_id) if resource_name in resources: resources[resource_name]["cost"] += cost else: deployments = fetch_openai_deployments(resource_id, access_token) resources[resource_name] = { "cost": cost, "deployments": deployments, "resource_id": resource_id, "is_foundry": True, } if daily and date is not None: daily_data.append({ "date": date, "resource_name": resource_name, "cost": cost, "subscription": subscription_name, }) if logging.getLogger().level == logging.DEBUG and resource_groups: logging.debug(f"Resource groups seen (all services): {resource_groups_seen}") if resource_groups: payload_rg = { "type": "AmortizedCost", **timeframe_config, "dataset": { "granularity": "Daily" if daily else "None", "aggregation": {"totalCost": {"name": "Cost", "function": "Sum"}}, "grouping": [ {"type": "Dimension", "name": "ResourceGroup"}, ], "filter": { "dimensions": { "name": "ResourceGroupName", "operator": "In", "values": resource_groups } } }, } response_rg = session.post(url, headers=headers, json=payload_rg) response_rg.raise_for_status() data_rg = response_rg.json() time.sleep(RATE_LIMIT_DELAY_SECONDS) rows_rg = data_rg.get("properties", {}).get("rows", []) logging.debug(f"Cost API returned {len(rows_rg)} rows for resource groups") if logging.getLogger().level == logging.DEBUG: logging.debug(f"Resource groups from config: {resource_groups}") rg_from_api = set() for row in rows_rg: resource_group = row[1] if len(row) > 1 else "" if resource_group: rg_from_api.add(resource_group.lower()) logging.debug(f"Resource groups returned by API: {rg_from_api}") logging.debug(f"Missing resource groups: {set(resource_groups) - rg_from_api}") for row in rows_rg: if daily: cost = row[0] date = row[1] if len(row) > 1 else None resource_group = row[2] if len(row) > 2 else "" else: cost = row[0] date = None resource_group = row[1] if len(row) > 1 else "" resource_group_lower = resource_group.lower() if resource_group_lower not in resources: resources[resource_group_lower] = { "cost": 0.0, "deployments": [], "resource_id": resource_group, "is_foundry": False, } resources[resource_group_lower]["cost"] += cost if daily and date is not None: daily_data.append( { "date": date, "resource_name": resource_group_lower, "cost": cost, "subscription": subscription_name, } ) return { "subscription": subscription_name, "resources": resources, "daily_data": daily_data if daily else [], } except requests.exceptions.RequestException as e: logging.error(f"Error fetching costs for subscription {subscription_name}: {e}") return {"subscription": subscription_name, "resources": None} def main(): access_token = get_azure_token() if not access_token: logging.error("Error: Unable to retrieve Azure access token.") logging.error("Please ensure you are logged in to Azure CLI: az login") return 1 parser = argparse.ArgumentParser( description="Fetch costs of LLM deployments in Azure subscriptions" ) parser.add_argument( "--timeframe", default="7days", help="Time period for cost analysis: 'this-week' (current week), '7days' (last 7 days, default), 'month-to-date', 'last-month' (previous complete month), '30days' (last 30 days), or specific month like 'nov-2025' or 'january-2026'", ) parser.add_argument( "--configure", nargs="?", const="config.yaml", metavar="CONFIG_FILE", help="Use a configuration file instead of discovering subscriptions (try config.yaml if no file specified)", ) parser.add_argument( "--daily-timeline", action="store_true", help="Generate daily cost timeline plots (resource_group_costs.png and total_costs.png)", ) parser.add_argument( "--verbose", action="store_true", help="Enable verbose logging (DEBUG level)", ) parser.add_argument( "--quiet", action="store_true", help="Suppress all logging except critical errors", ) args = parser.parse_args() if args.verbose: logging.getLogger().setLevel(logging.DEBUG) elif args.quiet: logging.getLogger().setLevel(logging.CRITICAL) if args.configure: config_file = args.configure logging.info(f"Loading subscriptions from {config_file}...") subscriptions = load_subscriptions(config_file) else: logging.info("Discovering subscriptions...") subscriptions = discover_subscriptions(access_token) if subscriptions: logging.info(f"Found {len(subscriptions)} subscriptions:") for sub in subscriptions: logging.info(f" - {sub['name']} ({sub['id']})") else: logging.error("No subscriptions found or error during discovery") return if not subscriptions: logging.error("No subscriptions found.") return start_date, end_date = get_date_range(args.timeframe) if not (start_date and end_date): logging.error( f"Invalid timeframe: '{args.timeframe}'. " f"Use one of: 'this-week', '7days', 'month-to-date', 'last-month', '30days', " f"or a specific month like 'nov-2025' or 'january-2026'." ) return 1 period_label = ( f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}" ) print(f"# Azure AI Foundry model costs (Amortized)\n**Date range: from {period_label}**\n\n*Costs are shown as amortized costs for the period, reflecting reserved instance amortization and commitment-based pricing.*") total_cost = 0.0 foundry_cost = 0.0 inhouse_cost = 0.0 all_daily_data = [] for idx, subscription in enumerate(subscriptions, 1): subscription_id = subscription["id"] subscription_name = subscription["name"] logging.info( f"[{idx}/{len(subscriptions)}] Fetching costs for {subscription_name}..." ) resource_groups = subscription.get("resource_groups", []) cost_data = fetch_llm_costs( subscription_id, subscription_name, access_token, args.timeframe, daily=args.daily_timeline, resource_groups=resource_groups, ) if cost_data["resources"] is not None: resources = cost_data["resources"] if resources: subscription_total = sum(res["cost"] for res in resources.values()) print(f"\n## {cost_data['subscription']}: ${subscription_total:.2f}") for resource_name, resource_info in sorted(resources.items()): print(f"### {resource_name}: ${resource_info['cost']:.2f}") if resource_info.get("is_foundry", False): foundry_cost += resource_info["cost"] if resource_info["deployments"]: for deployment in resource_info["deployments"]: created_by = deployment['createdBy'] created_at = deployment['createdAt'] if created_by: created_info = f" - Created by {created_by} at {created_at}" else: created_info = f" - Created at {created_at}" deployment_name = deployment['name'] if created_at != "Unknown" and start_date: try: created_date = datetime.fromisoformat(created_at.replace('Z', '+00:00')).date() if created_date >= start_date.date(): deployment_name = f"**NEW**: {deployment_name}" except (ValueError, AttributeError): pass print(f"- {deployment_name} ({deployment['model']}){created_info}") else: print("(Unable to fetch deployment details)") else: inhouse_cost += resource_info["cost"] total_cost += subscription_total else: logging.info(f"No Foundry model costs for {cost_data['subscription']}") else: print(f"\n## {cost_data['subscription']}: ERR\n(Error fetching costs)") if args.daily_timeline and cost_data.get("daily_data"): all_daily_data.extend(cost_data["daily_data"]) if idx < len(subscriptions): time.sleep(SUBSCRIPTION_DELAY_SECONDS) print("\n----\n") print(f"- **Total AI Foundry model costs: ${foundry_cost:.2f}**") print(f"- **Total in-house model costs: ${inhouse_cost:.2f}**") print(f"- **Total LLM operations costs: ${total_cost:.2f}**") if args.daily_timeline and all_daily_data: generate_timeline_plots(all_daily_data) if __name__ == "__main__": sys.exit(main() or 0) ```