Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Deploy, evaluate, and manage AI agents end-to-end on Microsoft Azure AI Foundry
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