| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 |
- """
- Custom build hook for Agency Swarm.
- This hook downloads the latest pricing data from LiteLLM before building the package.
- The pricing file is:
- 1. Downloaded automatically on `main` before each build (via this hook)
- 2. Included in the package artifacts (via pyproject.toml)
- 3. Committed to the repo so tests can run without network access
- """
- import importlib
- import logging
- import subprocess
- import tempfile
- from pathlib import Path
- BuildHookInterface: type = object
- try:
- _iface = importlib.import_module("hatchling.builders.hooks.plugin.interface")
- BuildHookInterface = _iface.BuildHookInterface
- except Exception:
- # Hatchling is a build-time dependency; it may not be installed in dev/test environments.
- # The build hook will still work when invoked by hatchling (where it is installed).
- BuildHookInterface = object
- logger = logging.getLogger(__name__)
- # URL to the LiteLLM pricing file
- PRICING_FILE_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
- # Target path relative to the project root (resolved via self.root in the hook)
- PRICING_FILE_RELATIVE_PATH = Path("src/agency_swarm/data/model_prices_and_context_window.json")
- def _warn_if_pricing_file_missing(pricing_file_path: Path) -> None:
- if pricing_file_path.exists():
- return
- logger.error(f"Pricing file not found at {pricing_file_path}. Usage cost tracking may be unavailable at runtime.")
- def _get_git_branch(root: str) -> str | None:
- try:
- proc = subprocess.run(
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
- cwd=root,
- check=True,
- capture_output=True,
- text=True,
- )
- except Exception:
- return None
- branch = proc.stdout.strip()
- return branch or None
- class CustomBuildHook(BuildHookInterface):
- """Build hook that downloads the latest pricing data before building."""
- def initialize(self, version, build_data):
- """Download pricing data file before build."""
- try:
- import urllib.request
- # Resolve path relative to the build root (hatchling may run from a different CWD)
- pricing_file_path = Path(self.root) / PRICING_FILE_RELATIVE_PATH
- # Ensure the data directory exists
- pricing_file_path.parent.mkdir(parents=True, exist_ok=True)
- branch = _get_git_branch(str(self.root))
- if branch != "main":
- # When builds skip the download (non-main branches or when git info isn't available),
- # still validate the pricing file exists so cost tracking doesn't silently disappear.
- _warn_if_pricing_file_missing(pricing_file_path)
- logger.info(
- "Skipping pricing data download (branch is not 'main'). "
- "Set branch to 'main' to auto-refresh this file during builds."
- )
- return
- logger.info(f"Downloading pricing data from {PRICING_FILE_URL}...")
- with urllib.request.urlopen(PRICING_FILE_URL) as resp:
- downloaded = resp.read()
- if pricing_file_path.exists():
- existing = pricing_file_path.read_bytes()
- if existing == downloaded:
- logger.info("Pricing data is already up to date; no file changes needed.")
- return
- with tempfile.NamedTemporaryFile(dir=pricing_file_path.parent, delete=False) as tmp:
- tmp.write(downloaded)
- tmp_path = Path(tmp.name)
- tmp_path.replace(pricing_file_path)
- logger.info(f"Successfully updated pricing data at {pricing_file_path}")
- # The file is already included in pyproject.toml artifacts, so we don't need to add it here
- # but we ensure it exists before the build proceeds
- except Exception as e:
- logger.warning(f"Failed to download pricing data: {e}. Build will continue with existing file if present.")
- # Don't fail the build if download fails - use existing file or handle gracefully
- pricing_file_path = Path(self.root) / PRICING_FILE_RELATIVE_PATH
- _warn_if_pricing_file_missing(pricing_file_path)
- # Export the hook class for hatchling to discover
- __all__ = ["CustomBuildHook"]
|