From 8aa1076ad897fb36f5c9607a4f52f042fdd6578c Mon Sep 17 00:00:00 2001 From: leecoder Date: Mon, 31 Aug 2026 18:26:54 +0900 Subject: [PATCH] feat(workflow): daily monitor of Databricks Foundation Model Serving pricing Adds a GitHub Action (daily 02:00 UTC + manual dispatch) that: - Scrapes the official DBU rates for DeepSeek V4 Flash (0731) / V4 Pro (0813) from https://www.databricks.com/product/pricing/foundation-model-serving - Derives per-token USD at $0.07/DBU and updates both model_prices_and_context_window.json and the packaged backup - Opens a PR (base litellm_internal_staging) only when a rate actually changed Runs on the leecoder fork (no repo guard) so the fork stays current and can feed an upstream PR on demand. --- .../workflows/monitor_databricks_pricing.py | 151 ++++++++++++++++++ .../workflows/monitor_databricks_pricing.yml | 56 +++++++ 2 files changed, 207 insertions(+) create mode 100644 .github/workflows/monitor_databricks_pricing.py create mode 100644 .github/workflows/monitor_databricks_pricing.yml diff --git a/.github/workflows/monitor_databricks_pricing.py b/.github/workflows/monitor_databricks_pricing.py new file mode 100644 index 00000000000..de645c3bd05 --- /dev/null +++ b/.github/workflows/monitor_databricks_pricing.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Monitor Databricks Foundation Model Serving pricing pages and update LiteLLM's +model_prices_and_context_window.json + packaged backup when rates change. + +Triggered daily by .github/workflows/monitor_databricks_pricing.yml. + +Behavior: +- Fetches the two official Databricks pricing pages (HTML, JS-rendered price + table). Parses the embedded price data rows via regex extraction of the + DBU table (works with the current page markup; fails loudly otherwise). +- Applies the LiteLLM convention: USD = DBU * 0.07 per token. +- Updates entries for the monitored model set (see MONITORED below). +- If any monitored rate changed, writes BOTH files, prints a diff summary and + exits 0 (so the workflow can create the PR). If nothing changed, exits 0 + with "NO_CHANGE" marker so the workflow skips PR creation. +""" + +import json +import re +import sys +import urllib.request +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +MAIN_MAP = REPO_ROOT / "model_prices_and_context_window.json" +BACKUP_MAP = REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json" + +DBU_TO_USD = 0.07 + +# Databricks Foundation Model Serving page (open models, incl. DeepSeek V4) +FMS_PAGE = "https://www.databricks.com/product/pricing/foundation-model-serving" +# Proprietary page (GPT/Claude/Gemini) — fetched but not used for the monitored +# monitorset; kept for future expansion. +PROPRIETARY_PAGE = "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + +# model_map key -> (name pattern in the DBU table row, ) +# name pattern is the model label as it appears on the pricing page table. +MONITORED = { + "databricks/databricks-deepseek-v4-flash-0731": "Deepseek V4 Flash (0731)", + "databricks/databricks-deepseek-v4-pro-0813": "Deepseek V4 Pro (0813)", +} + +# Context windows / output caps from Databricks Foundation Model APIs limits doc +# (kept in sync with what we know; only rates are refreshed by this script). +MODEL_FIXTURE = { + "databricks/databricks-deepseek-v4-flash-0731": { + "max_input_tokens": 200000, + "max_output_tokens": 10000, + }, + "databricks/databricks-deepseek-v4-pro-0813": { + "max_input_tokens": 200000, + "max_output_tokens": 4000, + }, +} + + +def fetch(url: str, max_bytes: int = 5_000_000) -> str: + """Fetch page HTML; returns text. Raises on non-200.""" + req = urllib.request.Request(url, headers={"User-Agent": "litellm-price-monitor/1.0"}) + with urllib.request.urlopen(req, timeout=60) as resp: + if resp.status != 200: + raise RuntimeError(f"HTTP {resp.status} fetching {url}") + return resp.read(max_bytes + 1).decode("utf-8", errors="replace") + + +def parse_dbu_table(html: str) -> dict[str, tuple[float, float]]: + rows: dict[str, tuple[float, float]] = {} + for tr in re.findall(r"(.*?)", html, flags=re.S): + cells = re.findall(r"]*>(.*?)", tr, flags=re.S) + if not cells: + continue + label = re.sub(r"<[^>]+>", "", cells[0]).strip() + nums = [] + for c in cells[1:]: + txt = re.sub(r"<[^>]+>", "", c).strip() + if re.fullmatch(r"\d+(?:\.\d+)?", txt): + nums.append(float(txt)) + if label and len(nums) >= 2: + rows[label] = (nums[0], nums[1]) + + results: dict[str, tuple[float, float]] = {} + for label in MONITORED.values(): + if label not in rows: + raise RuntimeError(f"Could not locate pricing row for '{label}' on {FMS_PAGE}") + results[label] = rows[label] + return results + + +def build_entry(model_key: str, input_dbu: float, output_dbu: float) -> dict: + fx = MODEL_FIXTURE[model_key] + return { + "input_cost_per_token": input_dbu / 1_000_000 * DBU_TO_USD, + "input_dbu_cost_per_token": input_dbu, + "litellm_provider": "databricks", + "max_input_tokens": fx["max_input_tokens"], + "max_output_tokens": fx["max_output_tokens"], + "max_tokens": fx["max_output_tokens"], + "metadata": { + "notes": ( + f"Pricing derived from Databricks Foundation Model Serving DBU rates " + f"({input_dbu:g} in / {output_dbu:g} out DBU per 1M tokens × ${DBU_TO_USD:.2f}/DBU " + f"= ${input_dbu*DBU_TO_USD:.2f}/${output_dbu*DBU_TO_USD:.2f} per 1M). " + f"Auto-refreshed daily by monitor_databricks_pricing workflow." + ) + }, + "mode": "chat", + "output_cost_per_token": output_dbu / 1_000_000 * DBU_TO_USD, + "output_dbu_cost_per_token": output_dbu, + "source": FMS_PAGE, + "supports_function_calling": True, + "supports_reasoning": True, + "supports_tool_choice": True, + } + + +def main() -> int: + html = fetch(FMS_PAGE) + rates = parse_dbu_table(html) + + with MAIN_MAP.open() as f: + main_data = json.load(f) + with BACKUP_MAP.open() as f: + backup_data = json.load(f) + + changed = False + for model_key, label in MONITORED.items(): + in_dbu, out_dbu = rates[label] + entry = build_entry(model_key, in_dbu, out_dbu) + old = main_data.get(model_key) + if old != entry: + main_data[model_key] = entry + backup_data[model_key] = entry + changed = True + print(f"CHANGED {model_key}: {old and old.get('input_cost_per_token')} -> {entry['input_cost_per_token']}") + + if not changed: + print("NO_CHANGE") + return 0 + + with MAIN_MAP.open("w") as f: + json.dump(main_data, f, indent=4) + f.write("\n") + with BACKUP_MAP.open("w") as f: + json.dump(backup_data, f, indent=4) + f.write("\n") + print("WROTE updated model map and backup") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/.github/workflows/monitor_databricks_pricing.yml b/.github/workflows/monitor_databricks_pricing.yml new file mode 100644 index 00000000000..ea2eaeefb2d --- /dev/null +++ b/.github/workflows/monitor_databricks_pricing.yml @@ -0,0 +1,56 @@ +name: Monitor Databricks Pricing (deepseek/glm/kimi) + +on: + schedule: + - cron: "0 2 * * *" # daily 02:00 UTC + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + monitor-db-pricing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Run monitor + id: monitor + run: | + python .github/workflows/monitor_databricks_pricing.py | tee /tmp/monitor_out.txt + if grep -q '^NO_CHANGE' /tmp/monitor_out.txt; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Open PR if changed + if: steps.monitor.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH="monitor-dbx-pricing-$(date +'%Y-%m-%d')" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add model_prices_and_context_window.json litellm/model_prices_and_context_window_backup.json + git commit -m "chore(model_prices): refresh Databricks Foundation Model Serving rates + +Automated daily monitor detected changed DBU rates on +https://www.databricks.com/product/pricing/foundation-model-serving +Updated entries: databricks/databricks-deepseek-v4-*" + git push origin "$BRANCH" + gh pr create --repo "${{ github.repository }}" \ + --base litellm_internal_staging \ + --head "$BRANCH" \ + --title "chore(model_prices): refresh Databricks Foundation Model Serving rates" \ + --body "Automated daily check of the Databricks Foundation Model Serving pricing page detected rate changes. Updated model_prices_and_context_window.json and packaged backup for monitored models." || true \ No newline at end of file