Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build and deploy AI applications on Azure AI Foundry using Microsoft's model catalog and AI services
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
finetuning/scripts/cleanup.py
1# /// script2# dependencies = [3# "openai>=1.0",4# "azure-identity",5# "azure-ai-projects",6# ]7# ///8"""9cleanup.py — Clean up fine-tuning resources to avoid quota exhaustion.1011Lists and optionally deletes uploaded files and cancels pending jobs.12Useful after experimentation to reclaim quota (max 100 files per resource,13deployment slots are limited).1415Usage:16python cleanup.py --list # List all resources17python cleanup.py --list --type files # List only files18python cleanup.py --delete-files --older-than 7 # Delete files older than 7 days19python cleanup.py --delete-files --dry-run # Preview what would be deleted20python cleanup.py --cancel-pending # Cancel queued jobs21"""2223import argparse24import os25import sys2627try:28sys.stdout.reconfigure(encoding="utf-8")29sys.stderr.reconfigure(encoding="utf-8")30except (AttributeError, OSError):31pass # Stream not reconfigurable (older Python or non-tty); default encoding is fine32from datetime import datetime, timezone3334sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))35from common import HelpOnErrorParser, get_clients363738def list_deployments(client):39"""List fine-tuned model deployments. Returns deployment info from jobs."""40deployments = []41for job in _iter_all_jobs(client):42if job.fine_tuned_model and job.status == "succeeded":43deployments.append({44"job_id": job.id,45"model": job.fine_tuned_model,46"base_model": job.model,47"created": datetime.fromtimestamp(job.created_at, tz=timezone.utc),48"tokens": job.trained_tokens,49})50return deployments515253def _iter_all_jobs(client, page_size=100):54"""Yield every fine-tuning job, paginating through the API.5556The OpenAI/Azure SDK's `jobs.list(limit=N)` returns at most N jobs with no57auto-paging. Users with >100 jobs would otherwise miss older jobs entirely.58"""59after = None60while True:61kwargs = {"limit": page_size}62if after:63kwargs["after"] = after64page = client.fine_tuning.jobs.list(**kwargs)65items = list(page)66if not items:67break68for job in items:69yield job70if len(items) < page_size:71break72# Cursor-based paging: use last job's id as `after`73after = items[-1].id747576def list_files(client):77"""List uploaded files."""78files = client.files.list()79result = []80for f in files:81result.append({82"id": f.id,83"filename": f.filename,84"purpose": f.purpose,85"bytes": f.bytes,86"created": datetime.fromtimestamp(f.created_at, tz=timezone.utc),87"status": f.status,88})89return result909192def list_jobs(client):93"""List fine-tuning jobs."""94result = []95for job in _iter_all_jobs(client):96result.append({97"id": job.id,98"status": job.status,99"model": job.model,100"fine_tuned_model": job.fine_tuned_model or "—",101"created": datetime.fromtimestamp(job.created_at, tz=timezone.utc),102})103return result104105106def format_age(dt):107"""Format a datetime as a human-readable age."""108delta = datetime.now(timezone.utc) - dt109if delta.days > 0:110return f"{delta.days}d ago"111hours = delta.seconds // 3600112return f"{hours}h ago"113114115def format_bytes(b):116"""Format bytes as human-readable size."""117if not b:118return "—"119if b > 1_000_000:120return f"{b/1_000_000:.1f} MB"121if b > 1_000:122return f"{b/1_000:.0f} KB"123return f"{b} B"124125126def show_list(client, resource_type="all"):127"""Display current resources."""128if resource_type in ("all", "jobs"):129jobs = list_jobs(client)130print(f"\n📋 Fine-tuning jobs ({len(jobs)}):")131if jobs:132print(f" {'ID':<25} {'Status':<12} {'Model':<20} {'Age':<10}")133print(f" {'-'*25} {'-'*12} {'-'*20} {'-'*10}")134for j in jobs:135print(f" {j['id'][:24]:<25} {j['status']:<12} {j['model']:<20} {format_age(j['created'])}")136else:137print(" (none)")138139if resource_type in ("all", "deployments"):140deps = list_deployments(client)141print(f"\n🚀 Fine-tuned models ({len(deps)}):")142if deps:143print(f" {'Model':<60} {'Age':<10}")144print(f" {'-'*60} {'-'*10}")145for d in deps:146name = d['model'][:59]147print(f" {name:<60} {format_age(d['created'])}")148else:149print(" (none)")150151if resource_type in ("all", "files"):152files = list_files(client)153print(f"\n📁 Uploaded files ({len(files)}):")154if files:155print(f" {'ID':<40} {'Size':>8} {'Purpose':<12} {'Age':<10} {'Status':<10}")156print(f" {'-'*40} {'-'*8} {'-'*12} {'-'*10} {'-'*10}")157for f in files:158print(f" {f['id']:<40} {format_bytes(f['bytes']):>8} {f['purpose']:<12} {format_age(f['created']):<10} {f['status']}")159else:160print(" (none)")161162# Quota warning163if len(files) >= 80:164print(f"\n ⚠️ {len(files)}/100 file slots used — approaching quota limit!")165166167def delete_files(client, older_than_days=None, dry_run=False):168"""Delete uploaded files, optionally filtering by age."""169files = list_files(client)170now = datetime.now(timezone.utc)171172to_delete = []173for f in files:174if older_than_days:175age_days = (now - f["created"]).days176if age_days < older_than_days:177continue178to_delete.append(f)179180if not to_delete:181print("No files to delete.")182return183184label = f"older than {older_than_days} days" if older_than_days else "all"185print(f"\n{'[DRY RUN] ' if dry_run else ''}Deleting {len(to_delete)} files ({label}):")186187deleted = 0188for f in to_delete:189print(f" {'Would delete' if dry_run else 'Deleting'}: {f['id']} ({f['filename']}, {format_age(f['created'])})")190if not dry_run:191try:192client.files.delete(f["id"])193deleted += 1194except Exception as e:195print(f" ❌ Failed: {e}")196197if not dry_run:198print(f"\n✅ Deleted {deleted}/{len(to_delete)} files")199200201def cancel_pending_jobs(client, dry_run=False):202"""Cancel any pending or queued jobs."""203jobs = list_jobs(client)204pending = [j for j in jobs if j["status"] in ("pending", "queued", "validating_files")]205206if not pending:207print("No pending jobs to cancel.")208return209210print(f"\n{'[DRY RUN] ' if dry_run else ''}Cancelling {len(pending)} pending jobs:")211for j in pending:212print(f" {'Would cancel' if dry_run else 'Cancelling'}: {j['id']} ({j['status']})")213if not dry_run:214try:215client.fine_tuning.jobs.cancel(j["id"])216except Exception as e:217print(f" ❌ Failed: {e}")218219220def build_parser():221parser = HelpOnErrorParser(222description="Clean up fine-tuning resources (deployments, files, jobs)",223epilog=(224"Examples:\n"225" python cleanup.py --list # Show all resources\n"226" python cleanup.py --list --type files # Show files only\n"227" python cleanup.py --delete-files --older-than 7 # Delete files older than 7 days\n"228" python cleanup.py --delete-files --dry-run # Preview what would be deleted\n"229" python cleanup.py --cancel-pending # Cancel queued jobs"230),231formatter_class=argparse.RawTextHelpFormatter,232)233parser.add_argument("--base-url", default=os.environ.get("OPENAI_BASE_URL"), help="Project /v1/ endpoint URL")234parser.add_argument("--endpoint", default=os.environ.get("AZURE_OPENAI_ENDPOINT"),235help="Azure OpenAI endpoint (fallback)")236parser.add_argument("--api-key", default=os.environ.get("AZURE_OPENAI_API_KEY"), help="API key")237parser.add_argument("--project-endpoint", default=os.environ.get("AZURE_AI_PROJECT_ENDPOINT"),238help="Azure AI project endpoint")239240parser.add_argument("--list", action="store_true", help="List resources")241parser.add_argument("--type", choices=["all", "jobs", "deployments", "files"], default="all",242help="Resource type to list (default: all)")243244parser.add_argument("--delete-files", action="store_true", help="Delete uploaded files")245parser.add_argument("--older-than", type=int, default=None,246help="Only delete files older than N days (use with --delete-files)")247parser.add_argument("--cancel-pending", action="store_true", help="Cancel pending/queued jobs")248parser.add_argument("--dry-run", action="store_true", help="Preview changes without executing")249return parser250251252if __name__ == "__main__":253parser = build_parser()254if len(sys.argv) == 1:255parser.print_help()256sys.exit(0)257258args = parser.parse_args()259client, method = get_clients(base_url=args.base_url, azure_endpoint=args.endpoint, project_endpoint=args.project_endpoint, api_key=args.api_key)260261if args.list:262show_list(client, args.type)263264if args.delete_files:265delete_files(client, older_than_days=args.older_than, dry_run=args.dry_run)266267if args.cancel_pending:268cancel_pending_jobs(client, dry_run=args.dry_run)269