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.
This commit is contained in:
leecoder 2026-08-31 18:26:54 +09:00
parent 1009976c49
commit 8aa1076ad8
2 changed files with 207 additions and 0 deletions

View file

@ -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"<tr>(.*?)</tr>", html, flags=re.S):
cells = re.findall(r"<t[hd][^>]*>(.*?)</t[hd]>", 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())

View file

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