chore: import upstream snapshot with attribution
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:44:17 +08:00
commit 85742ab165
588 changed files with 320176 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Aggregates the “latest completed” results of several dependent workflows and fails
* this step if any required job/variant is missing or not successful.
*
* Usage (from actions/github-script@v8):
*
* const badgeAggregation = require('./scripts/badge_aggregation.js');
* const dependencies = [
* { workflow: 'examples-calc-x.yml', label: 'calc-x.latest', variants: ['latest'] },
* { workflow: 'examples-spider.yml', label: 'spider.latest', variants: ['latest'] },
* { workflow: 'examples-apo.yml', label: 'apo.latest', variants: ['latest'] },
* { workflow: 'examples-unsloth.yml', label: 'unsloth.latest', variants: ['latest'] },
* { workflow: 'tests-full.yml', label: 'tests-full.latest', variants: ['latest'] },
* ];
* await badgeAggregation({ github, context, core, dependencies });
*
* Notes:
* - Requires the workflow files above to exist in .github/workflows/.
* - Looks at the default branch "main" unless you override per dependency with dep.branch.
* - Assumes matrix job names contain the variant in parentheses, e.g. "tests (latest)".
*/
module.exports = async function badgeAggregation({ github, context, core, dependencies }) {
const failures = [];
// Defensive: validate inputs early for nicer error messages.
if (!github?.rest?.actions || !context?.repo || !core) {
throw new Error('badgeAggregation: expected { github, context, core } from actions/github-script.');
}
if (!Array.isArray(dependencies) || dependencies.length === 0) {
core.info('No dependencies provided; nothing to check.');
return;
}
// Helper: paginate jobs for a run attempt (handles >100 jobs edge case).
async function listAllJobsForAttempt(run_id, attempt_number) {
const all = [];
let page = 1;
while (true) {
const { data } = await github.rest.actions.listJobsForWorkflowRunAttempt({
owner: context.repo.owner,
repo: context.repo.repo,
run_id,
attempt_number,
per_page: 100,
page,
});
const jobs = data.jobs ?? [];
all.push(...jobs);
if (!data.total_count || all.length >= data.total_count || jobs.length === 0) break;
page += 1;
}
return all;
}
// For each dependency: find the latest completed run on the target branch; then inspect its jobs.
for (const dep of dependencies) {
const branch = dep.branch || 'main';
// You can pass the workflow file name as workflow_id (e.g. "examples-apo.yml").
const { data: runsData } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: dep.workflow,
branch: 'main', // Always check the main branch status no matter what
status: 'completed', // only completed runs
per_page: 50, // retrieve latest 50 so we can filter
sort: 'created',
direction: 'desc',
});
const filteredRuns = runsData?.workflow_runs?.filter(run => ['schedule', 'workflow_dispatch'].includes(run.event));
const run = filteredRuns?.[0];
if (!run) {
failures.push(`No completed run found for ${dep.label} on branch "${branch}"`);
continue;
}
core.info(`[${dep.label}] Found run ${run.id} with attempt ${run.run_attempt}`);
// Get the specific attempt we want to inspect (latest attempt for that run).
const attempt = run.run_attempt ?? 1;
// Robust: paginate jobs in case the workflow has many.
const jobs = await listAllJobsForAttempt(run.id, attempt);
core.info(`[${dep.label}] Found ${jobs.length} jobs: ${jobs.map(j => j.name).join(', ')}`);
// Match each required variant to a job. We look for the variant in parentheses, e.g. "(latest)".
for (const variant of dep.variants || []) {
const matchingJobs = jobs.filter(
j => typeof j.name === 'string' && j.name.includes(variant)
);
if (matchingJobs.length === 0) {
failures.push(`Missing job for ${dep.label} (variant: ${variant})`);
continue;
}
for (const job of matchingJobs) {
core.info(`[${dep.label}] ${job.name} => ${job.conclusion}`);
// Accept only a strict "success".
if (job.conclusion !== 'success') {
failures.push(`${dep.label} (${job.name}) concluded ${job.conclusion}`);
}
}
}
}
// Surface aggregated result to the workflow.
if (failures.length) {
core.setFailed(failures.join(' | '));
} else {
core.info('All latest variants succeeded.');
}
};
+19
View File
@@ -0,0 +1,19 @@
FROM nvidia/cuda:12.8.0-cudnn-devel-ubuntu24.04
RUN apt-get update && apt-get install -y \
git \
wget \
curl \
build-essential \
python3-dev \
python3-pip \
python3-venv \
graphviz \
unzip \
tmux \
vim \
git-lfs && \
git lfs install && \
rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
# Based on: Standard_NC24ads_A100_v4
# With: Canonical:ubuntu-24_04-lts:server:latest
# Secure boot is off.
# This script is not designed to be run automatically.
set -ex
# Build essentials are required.
sudo apt-get clean
sudo apt-get update
sudo apt-get install -y \
git \
wget \
curl \
build-essential \
software-properties-common \
python3-dev \
python3-pip \
python3-venv \
graphviz \
unzip \
tmux \
vim \
git-lfs \
nodejs \
gnupg2 \
apt-transport-https \
ca-certificates \
gnupg \
lsb-release
git lfs install
# VM with GPU needs to install drivers. Reference:
# https://docs.microsoft.com/en-us/azure/virtual-machines/linux/n-series-driver-setup
sudo apt update && sudo apt install -y ubuntu-drivers-common
sudo ubuntu-drivers install
sudo reboot now
# Install CUDA Toolkit
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb && rm cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda-toolkit
sudo reboot now
# Add paths globally
sudo bash -c "cat > /etc/profile.d/cuda.sh" <<'EOF'
export PATH=/usr/local/cuda/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH
EOF
sudo chmod +x /etc/profile.d/cuda.sh
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt -y update
# Install the Docker packages
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Create docker group only if it doesn't exist
# sudo groupadd docker
# Add current user to docker group if not already a member
sudo usermod -aG docker "$USER"
# A hack to add cloudtest user to docker group as well
sudo sed -i '/^docker:/ s/$/,cloudtest/' /etc/group
# This shouldn't be run on CI
# newgrp docker
# Install NVIDIA Container Toolkit
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.18.0-1
sudo apt-get install -y \
nvidia-container-toolkit=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
nvidia-container-toolkit-base=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
libnvidia-container-tools=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
libnvidia-container1=${NVIDIA_CONTAINER_TOOLKIT_VERSION}
# Configure the NVIDIA Container Toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# Install Azure CLI
curl -sLS https://packages.microsoft.com/keys/microsoft.asc |
gpg --dearmor | sudo tee /etc/apt/keyrings/microsoft.gpg > /dev/null
sudo chmod go+r /etc/apt/keyrings/microsoft.gpg
AZ_DIST=$(lsb_release -cs)
echo "Types: deb
URIs: https://packages.microsoft.com/repos/azure-cli/
Suites: ${AZ_DIST}
Components: main
Architectures: $(dpkg --print-architecture)
Signed-by: /etc/apt/keyrings/microsoft.gpg" | sudo tee /etc/apt/sources.list.d/azure-cli.sources
sudo apt-get update
sudo apt-get install -y azure-cli
# Disable the periodical apt-get upgrade.
# Sometimes, unattended upgrade blocks apt-get install
sudo sed -i -e "s/Update-Package-Lists \"1\"/Update-Package-Lists \"0\"/g" /etc/apt/apt.conf.d/10periodic
sudo sed -i -e "s/Update-Package-Lists \"1\"/Update-Package-Lists \"0\"/g" /etc/apt/apt.conf.d/20auto-upgrades
sudo sed -i -e "s/Unattended-Upgrade \"1\"/Unattended-Upgrade \"0\"/g" /etc/apt/apt.conf.d/20auto-upgrades
sudo systemctl stop apt-daily.timer apt-daily-upgrade.timer
sudo systemctl stop apt-daily.service apt-daily-upgrade.service
sudo systemctl mask apt-daily.timer apt-daily-upgrade.timer
sudo systemctl mask apt-daily.service apt-daily-upgrade.service
# Deprovision and prepare for generalized image
sudo waagent -deprovision+user
+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
# Script to bump version in pyproject.toml
# Usage: ./bump_version.sh <new_version>
set -e
# Check if version argument is provided
if [ $# -eq 0 ]; then
echo "Error: No version specified"
echo "Usage: $0 <new_version>"
echo "Examples: $0 1.2.3, $0 1.2.3a1, $0 1.2.3b2, $0 1.2.3rc1, $0 1.2.3.post1, $0 1.2.3.dev1"
exit 1
fi
NEW_VERSION="$1"
# Validate version format (Python PEP 440 compliant)
if ! echo "$NEW_VERSION" | grep -E '^[0-9]+(\.[0-9]+)*((a|b|rc)[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?$' > /dev/null; then
echo "Error: Invalid version format"
echo "Version should follow PEP 440 (e.g., 1.2.3, 1.2.3a1, 1.2.3b2, 1.2.3rc1, 1.2.3.post1, 1.2.3.dev1)"
exit 1
fi
# Get the directory where the script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
PYPROJECT_FILE="$PROJECT_ROOT/pyproject.toml"
# Check if pyproject.toml exists
if [ ! -f "$PYPROJECT_FILE" ]; then
echo "Error: pyproject.toml not found at $PYPROJECT_FILE"
exit 1
fi
# Get current version
CURRENT_VERSION=$(grep '^version = ' "$PYPROJECT_FILE" | sed 's/version = "\(.*\)"/\1/')
if [ -z "$CURRENT_VERSION" ]; then
echo "Error: Could not find current version in pyproject.toml"
exit 1
fi
echo "Current version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Update version in pyproject.toml
sed -i.bak "s/^version = \".*\"/version = \"$NEW_VERSION\"/" "$PYPROJECT_FILE"
# Remove backup file
rm -f "$PYPROJECT_FILE.bak"
echo "Successfully bumped version from $CURRENT_VERSION to $NEW_VERSION"
# Update __init__.py if it exists with version
INIT_FILE="$PROJECT_ROOT/agentlightning/__init__.py"
if [ -f "$INIT_FILE" ]; then
if grep -q "__version__" "$INIT_FILE"; then
sed -i.bak "s/__version__ = \".*\"/__version__ = \"$NEW_VERSION\"/" "$INIT_FILE"
rm -f "$INIT_FILE.bak"
echo "Updated version in $INIT_FILE"
fi
fi
echo "Version bump complete!"
+105
View File
@@ -0,0 +1,105 @@
# Copyright (c) Microsoft. All rights reserved.
"""Ensure tracked source files include the required copyright header."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
HEADER_TEXT = "Copyright (c) Microsoft. All rights reserved."
REPO_ROOT = Path(__file__).resolve().parent.parent
COMMENT_PREFIX_BY_SUFFIX: dict[str, str] = {
".py": "#",
".pyi": "#",
".pyw": "#",
".js": "//",
".jsx": "//",
".ts": "//",
".tsx": "//",
".mjs": "//",
".mts": "//",
".cjs": "//",
".cts": "//",
}
REQUIRED_HEADER_BY_SUFFIX = {
suffix: f"{prefix} {HEADER_TEXT}" if not prefix.endswith(" ") else f"{prefix}{HEADER_TEXT}"
for suffix, prefix in COMMENT_PREFIX_BY_SUFFIX.items()
}
def iter_source_files() -> list[Path]:
"""Return tracked source files matching supported extensions."""
if not REQUIRED_HEADER_BY_SUFFIX:
return []
pathspecs = [f"*{suffix}" for suffix in sorted(REQUIRED_HEADER_BY_SUFFIX)]
result = subprocess.run(
[
"git",
"ls-files",
"--cached",
"--others",
"--exclude-standard",
"--",
*pathspecs,
],
capture_output=True,
text=True,
check=True,
cwd=REPO_ROOT,
)
return [REPO_ROOT / line.strip() for line in result.stdout.splitlines() if line.strip()]
def main() -> int:
missing_header: list[str] = []
missing_blank_line: list[str] = []
for file_path in iter_source_files():
expected_header = REQUIRED_HEADER_BY_SUFFIX.get(file_path.suffix.lower())
if expected_header is None:
continue
if not file_path.exists():
continue
try:
with file_path.open("r", encoding="utf-8") as file:
first_line = file.readline().rstrip("\r\n")
second_line = file.readline()
except OSError as exc:
print(f"Failed to read {file_path}: {exc}", file=sys.stderr)
return 1
if first_line != expected_header:
missing_header.append(str(file_path.relative_to(REPO_ROOT)))
continue
# Second line should be either an EOF or a blank line
if second_line and second_line.strip():
missing_blank_line.append(str(file_path.relative_to(REPO_ROOT)))
if missing_header:
print("The following files are missing the required copyright header:")
for path in missing_header:
print(f" - {path}")
header_examples = "\n".join(sorted(set(REQUIRED_HEADER_BY_SUFFIX.values())))
print(f"Run the appropriate script or add the header manually:\n{header_examples}")
if missing_blank_line:
print("The following files are missing a blank line after the copyright header:")
for path in missing_blank_line:
print(f" - {path}")
print("Ensure there is an empty line separating the header from the rest of the file.")
if missing_header or missing_blank_line:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+6
View File
@@ -0,0 +1,6 @@
set -ex
rm -rf examples/spider/checkpoints
rm -rf examples/calc_x/checkpoints
rm -rf examples/unsloth/models
rm -rf verl
+87
View File
@@ -0,0 +1,87 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import requests
from openai import OpenAI
# Most common Azure OpenAI setup:
# AZURE_OPENAI_ENDPOINT="https://<resource>.openai.azure.com"
# AZURE_OPENAI_API_KEY="..."
# Optional (only if your endpoint requires it):
# AZURE_OPENAI_API_VERSION="2025-xx-xx"
#
# This script treats "delete finetune job" as "cancel finetune job"
# because fine-tune jobs are typically cancellable, not deletable.
def _client() -> OpenAI:
# This script assumes AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY are set in the environment.
endpoint = os.environ["AZURE_OPENAI_ENDPOINT"]
api_key = os.environ["AZURE_OPENAI_API_KEY"]
return OpenAI(api_key=api_key, base_url=endpoint)
def list_data_files():
c = _client()
return c.files.list(limit=100)
def list_finetune_jobs():
c = _client()
return c.fine_tuning.jobs.list(limit=100)
def delete_data_file(file_id: str):
c = _client()
return c.files.delete(file_id)
def cancel_finetune_job(job_id: str):
c = _client()
return c.fine_tuning.jobs.cancel(job_id)
def delete_finetune_job(job_id: str):
# This script assumes AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY are set in the environment.
endpoint = os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/")
api_key = os.environ["AZURE_OPENAI_API_KEY"]
root = endpoint.split("/openai")[0]
url = f"{root}/openai/fine_tuning/jobs/{job_id}"
params = {"api-version": os.environ["AZURE_OPENAI_API_VERSION"]}
resp = requests.delete(url, headers={"api-key": api_key}, params=params, timeout=60)
resp.raise_for_status()
return resp.content
if __name__ == "__main__":
# Quick demo: print IDs you could delete
jobs = list_finetune_jobs().data
files = list_data_files().data
print("JOBS:")
for j in jobs:
print(f" {j.id} {getattr(j, 'status', '')} {getattr(j, 'model', '')}")
print("\nFILES:")
for f in files:
print(f" {f.id} {getattr(f, 'filename', '')} {getattr(f, 'status', '')}")
# Delete them all WITHOUT CONFIRMATION!
for j in jobs:
print(f"Deleting job {j.id}")
try:
if j.status == "running":
cancel_finetune_job(j.id)
delete_finetune_job(j.id)
except Exception as exc:
print(f" Error deleting job {j.id}: {exc}")
for f in files:
print(f"Deleting file {f.id}")
try:
delete_data_file(f.id)
except Exception as exc:
print(f" Error deleting file {f.id}: {exc}")
+26
View File
@@ -0,0 +1,26 @@
# Copyright (c) Microsoft. All rights reserved.
"""Generate OpenAPI specification for the LightningStore server.
Run this every time when you make changes to the LightningStore server.
"""
import asyncio
import json
from agentlightning.store.client_server import LightningStoreServer
from agentlightning.store.memory import InMemoryLightningStore
async def main():
store = InMemoryLightningStore()
server = LightningStoreServer(store, host="0.0.0.0", port=23333)
await server.start()
with open("docs/assets/store-openapi.json", "w") as f:
json.dump(server.app.openapi(), f) # type: ignore
await server.stop()
if __name__ == "__main__":
asyncio.run(main())
+53
View File
@@ -0,0 +1,53 @@
model_list:
- model_name: gpt-4o
litellm_params:
model: azure/gpt-4o
api_base: os.environ/AZURE_API_BASE
api_version: 2025-04-01-preview
api_key: os.environ/AZURE_API_KEY
- model_name: gpt-4o-mini
litellm_params:
model: azure/gpt-4o-mini
api_base: os.environ/AZURE_API_BASE
api_version: 2025-04-01-preview
api_key: os.environ/AZURE_API_KEY
- model_name: gpt-4.1
litellm_params:
model: azure/gpt-4.1
api_base: os.environ/AZURE_API_BASE
api_version: 2025-04-01-preview
api_key: os.environ/AZURE_API_KEY
- model_name: gpt-4.1-mini
litellm_params:
model: azure/gpt-4.1-mini
api_base: os.environ/AZURE_API_BASE
api_version: 2025-04-01-preview
api_key: os.environ/AZURE_API_KEY
- model_name: gpt-4.1-nano
litellm_params:
model: azure/gpt-4.1-nano
api_base: os.environ/AZURE_API_BASE
api_version: 2025-04-01-preview
api_key: os.environ/AZURE_API_KEY
- model_name: o4-mini
litellm_params:
model: azure/o4-mini
api_base: os.environ/AZURE_API_BASE
api_version: 2025-04-01-preview
api_key: os.environ/AZURE_API_KEY
- model_name: gpt-5-nano
litellm_params:
model: azure/gpt-5-nano
api_base: os.environ/AZURE_API_BASE
api_version: 2025-04-01-preview
api_key: os.environ/AZURE_API_KEY
- model_name: gpt-5-mini
litellm_params:
model: azure/gpt-5-mini
api_base: os.environ/AZURE_API_BASE
api_version: 2025-04-01-preview
api_key: os.environ/AZURE_API_KEY
general_settings:
# https://github.com/BerriAI/litellm/issues/15952
disable_responses_id_security: true
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
export
# Configurable port (first CLI argument, or default to 12306)
PORT="${1:-12306}"
# Launch LiteLLM Proxy in background
echo "Starting LiteLLM Proxy on port ${PORT}..."
nohup uv run litellm --config scripts/litellm_ci.yaml --port "${PORT}" &
# Wait for the server to be ready
echo "Waiting for LiteLLM Proxy to start..."
for i in {1..30}; do
if curl -s "http://localhost:${PORT}/v1/models" > /dev/null; then
echo "LiteLLM Proxy is up!"
break
fi
echo "Waiting... (${i})"
# Wait for 2 seconds before checking again
sleep 2
done
# Run sanity check
echo "Running sanity check..."
export OPENAI_BASE_URL="http://localhost:${PORT}/"
export OPENAI_API_KEY="dummy"
uv run scripts/litellm_sanity_check.py
echo "Sanity check complete!"
+55
View File
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft. All rights reserved.
"""Utility script to perform a sanity check on LiteLLM proxy server."""
import sys
import openai
def main() -> None:
client = openai.OpenAI(timeout=30.0)
models = client.models.list()
print("Available models:", models)
total_requests = 0
success_count = 0
for model in models.data:
try:
total_requests += 1
response = client.chat.completions.create(
model=model.id,
messages=[{"role": "user", "content": "Hello!"}],
)
print(f"Chat completion from model {model.id}:", response)
success_count += 1
except Exception as e:
print(f"Chat completion failed for model {model.id}: {e}")
try:
total_requests += 1
response = client.responses.create(
model=model.id,
input="Hello, world!",
)
print(f"Response from model {model.id}:", response)
success_count += 1
except Exception as e:
print(f"Response failed for model {model.id}: {e}")
if total_requests == 0:
print("No requests made.")
sys.exit(1)
success_rate = success_count / total_requests
print(f"Success rate: {success_rate * 100:.2f}% ({success_count}/{total_requests})")
if success_rate >= 0.8:
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
set -euo pipefail
cd docker
# Setup data directories
./setup.sh
# Start Dockers
docker compose -f compose.mongo.yml up -d
SERVICE_NAME=mongo
TIMEOUT=60 # seconds
SLEEP=2
cid="$(docker compose -f compose.mongo.yml ps -q "$SERVICE_NAME")"
if [ -z "$cid" ]; then
echo "Service $SERVICE_NAME is not running"
exit 1
fi
echo "Waiting for $SERVICE_NAME to become healthy..."
end=$((SECONDS + TIMEOUT))
while [ "$SECONDS" -lt "$end" ]; do
status="$(docker inspect -f '{{.State.Health.Status}}' "$cid")"
echo "Current status: $status"
if [ "$status" = "healthy" ]; then
echo "$SERVICE_NAME is healthy ✅"
exit 0
elif [ "$status" = "unhealthy" ]; then
echo "$SERVICE_NAME is unhealthy ❌"
docker logs "$cid" || true
exit 1
fi
sleep "$SLEEP"
done
echo "Timed out waiting for $SERVICE_NAME to become healthy after ${TIMEOUT}s"
docker logs "$cid" || true
exit 1
+9
View File
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
// MongoDB replica set initialization script.
// Use this if you are accessing MongoDB from the **host**.
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "localhost:27017" }],
});
+12
View File
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
// MongoDB replica set initialization script.
// Use this if you are accessing MongoDB from another **container**.
// `mongodb_init_rs_host.js` is the counterpart if accessing from the host.
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "mongo:27017" }],
});
db.setProfilingLevel(2);
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -ex
ray stop -v --force --grace-period 60
ps aux
env RAY_DEBUG=legacy HYDRA_FULL_ERROR=1 VLLM_USE_V1=1 ray start --head --dashboard-host=0.0.0.0
+5
View File
@@ -0,0 +1,5 @@
set -ex
python -m pip install --upgrade --no-cache-dir pip
pip install --no-cache-dir -e .[dev,agent,apo]
# Upgrade agentops to the latest version
pip install --no-cache-dir -U agentops
+17
View File
@@ -0,0 +1,17 @@
set -ex
python -m pip install --upgrade --no-cache-dir pip
pip install --no-cache-dir packaging ninja numpy pandas ipython ipykernel gdown wheel setuptools
# This has to be pinned for VLLM to work.
pip install --no-cache-dir torch==2.8.0 torchvision==0.23.0 --index-url https://download.pytorch.org/whl/cu128
pip install --no-cache-dir flash-attn --no-build-isolation
# This must match pytorch version.
pip install --no-cache-dir vllm==0.10.2
# Latest VERL release version.
# FIXME: Make VERL 0.5.0 work
pip install --no-cache-dir "verl<0.6.0"
pip install --no-cache-dir -e .[dev,agent,trl,apo]
# Upgrade agentops to the latest version
pip install --no-cache-dir -U agentops
+3
View File
@@ -0,0 +1,3 @@
set -ex
python -m pip install --upgrade --no-cache-dir pip
pip install --no-cache-dir -e .[dev,agent,apo]
+13
View File
@@ -0,0 +1,13 @@
set -ex
python -m pip install --upgrade pip
pip install --no-cache-dir packaging ninja numpy pandas ipython ipykernel gdown wheel setuptools
pip install --no-cache-dir torch==2.7.0 torchvision==0.22.0 torchaudio==2.7.0 --index-url https://download.pytorch.org/whl/cu128
pip install --no-cache-dir --no-deps trl unsloth # For type checking, not for running examples
pip install --no-cache-dir transformers==4.53.3
pip install --no-cache-dir flash-attn==2.8.1 --no-build-isolation
pip install --no-cache-dir vllm==0.9.2
pip install --no-cache-dir verl==0.5.0
pip install --no-cache-dir -e .[dev,agent,apo]
+11
View File
@@ -0,0 +1,11 @@
set -ex
python -m pip install --upgrade --no-cache-dir pip
# CPU version full installation
pip install --no-cache-dir packaging ninja numpy pandas ipython ipykernel gdown wheel setuptools
pip install --no-cache-dir vllm # pytorch auto installed when installing vllm
pip install --no-cache-dir --no-deps trl unsloth
pip install --no-cache-dir verl==0.5.0
pip install --no-cache-dir -e .[dev,agent,apo]
+106
View File
@@ -0,0 +1,106 @@
# Copyright (c) Microsoft. All rights reserved.
import argparse
import sys
import wandb
def parse_args():
parser = argparse.ArgumentParser(description="Validate a Weights & Biases run for reward/trace rollouts.")
parser.add_argument("project", help="W&B project name")
parser.add_argument("run_name", help="W&B run display name")
parser.add_argument(
"--reward-tolerance",
type=int,
default=0,
help="Allowed difference between first and last val/n_rollouts_w_reward",
)
parser.add_argument(
"--trace-tolerance",
type=int,
default=0,
help="Allowed difference between first and last val/n_rollouts_w_trace",
)
return parser.parse_args()
args = parse_args()
project = args.project
run_name = args.run_name
api = wandb.Api()
entity_name = api.default_entity
print("Default entity:", entity_name)
print("Project:", project)
print("Run name:", run_name)
runs = api.runs(f"{entity_name}/{project}", filters={"displayName": run_name})
for run in runs:
print(f"Found run: {run.name} (ID: {run.id})")
if run.name == run_name:
break
else:
print(f"::error::Run with name '{run_name}' not found in project '{project}'.")
sys.exit(1)
hist = run.history(
keys=["val/reward", "val/n_rollouts_w_reward", "val/n_rollouts_w_trace", "val/mean_response_length"], pandas=True
)
print("History:", hist)
if hist.empty:
print("::error::No history found for the run.")
sys.exit(1)
else:
# Check whether all rollouts have (approximately) succeeded
first_row = hist.iloc[0]
last_row = hist.iloc[-1]
first_reward_rollouts = first_row["val/n_rollouts_w_reward"]
last_reward_rollouts = last_row["val/n_rollouts_w_reward"]
reward_diff = abs(first_reward_rollouts - last_reward_rollouts)
if reward_diff > args.reward_tolerance or (first_reward_rollouts == 0 and last_reward_rollouts == 0):
print(
"::error::Some rollouts have failed to produce rewards: "
f"{first_reward_rollouts} -> {last_reward_rollouts} "
f"(tolerance={args.reward_tolerance})"
)
sys.exit(1)
elif first_reward_rollouts != last_reward_rollouts:
print(
"::warning::First and last val/n_rollouts_w_reward are different: "
f"{first_reward_rollouts} -> {last_reward_rollouts}"
)
first_trace_rollouts = first_row["val/n_rollouts_w_trace"]
last_trace_rollouts = last_row["val/n_rollouts_w_trace"]
trace_diff = abs(first_trace_rollouts - last_trace_rollouts)
if trace_diff > args.trace_tolerance or (first_trace_rollouts == 0 and last_trace_rollouts == 0):
print(
"::error::Some rollouts have failed to produce traces: "
f"{first_trace_rollouts} -> {last_trace_rollouts} "
f"(tolerance={args.trace_tolerance})"
)
sys.exit(1)
elif first_trace_rollouts != last_trace_rollouts:
print(
"::warning::First and last val/n_rollouts_w_trace are different: "
f"{first_trace_rollouts} -> {last_trace_rollouts}"
)
val_mean_response = last_row["val/mean_response_length"]
if val_mean_response < 1:
print(f"::error::Mean response length is too short: {val_mean_response} (expected >= 1)")
sys.exit(1)
first_reward, last_reward = first_row["val/reward"], last_row["val/reward"]
if last_reward <= first_reward:
print(
f"::warning title=Training no improvement::No improvement (run_name={run_name} start={first_reward:.4f}, end={last_reward:.4f})"
)
else:
print(
f"::notice title=Training completed::Run has improved (run_name={run_name} start={first_reward:.4f}, end={last_reward:.4f})"
)
+249
View File
@@ -0,0 +1,249 @@
# Copyright (c) Microsoft. All rights reserved.
"""Usage example:
python scripts/wandb_download_result.py AgentLightning \
--runs spider_agl_v0_2 \
--metrics training/reward val/reward \
--out docs/assets/sql-agent-training-result.json \
--step 16
"""
import argparse
import json
import sys
from typing import Any, Dict, List, Tuple
import numpy as np
import pandas as pd
import wandb
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description=(
"Fetch metrics from Weights & Biases runs and output Chart.js-ready JSON. "
"Aggregates by step bins to tame long x-axes."
)
)
p.add_argument(
"project",
help="W&B project name (e.g., 'my-project'). Uses your default entity unless --entity is set.",
)
p.add_argument(
"--entity",
default=None,
help="W&B entity (team/user). If omitted, uses wandb.Api().default_entity.",
)
p.add_argument(
"--runs",
nargs="+",
required=True,
help="Run names (display names) to include. Example: --runs a b c",
)
p.add_argument(
"--metrics",
nargs="+",
required=True,
help="Metric keys to fetch. Example: --metrics train/loss val/acc",
)
p.add_argument(
"--step",
type=int,
default=1,
help="Aggregate step size in _step units (e.g., 16 groups steps into bins of 16). Default: 1 (no binning).",
)
p.add_argument(
"--out",
default="wandb_result.json",
help="Output file name. Default: 'wandb_result.json'",
)
p.add_argument(
"--label-format",
default="{run}:{metric}",
help="Dataset label format. You can use {run} and {metric}. Default: '{run}:{metric}'",
)
p.add_argument(
"--strict",
action="store_true",
help="If set, exit with nonzero code when a run or metric is missing.",
)
return p.parse_args()
def fetch_runs(api: wandb.Api, entity: str, project: str, run_names: List[str]) -> Dict[str, wandb.Run]:
"""
Fetch runs by displayName matching any in run_names.
"""
name_set = set(run_names)
found: Dict[str, wandb.Run] = {}
# W&B filtering supports 'displayName'
# We fetch all runs in the project once, then pick matching ones to be robust across filters/backends.
# If the project is huge, you can optimize to paginate/stop early—here we walk until weve found all.
for run in api.runs(f"{entity}/{project}"):
dn = getattr(run, "name", None) or getattr(run, "displayName", None)
# run.name is usually the short name; W&B Python public API exposes it as .name
if dn in name_set and dn not in found:
found[dn] = run
if len(found) == len(name_set):
break
return found
def aggregate_history(df: pd.DataFrame, metrics: List[str], step: int) -> pd.DataFrame:
"""
Given a history dataframe with '_step' and metric columns,
aggregate by floor(_step/step)*step and average metric values per bin.
"""
if "_step" not in df.columns:
raise ValueError("History dataframe missing required '_step' column.")
if step < 1:
step = 1
# Drop rows where all requested metrics are NaN to avoid empty bins
keep_mask = df[metrics].notna().any(axis=1)
df = df.loc[keep_mask].copy()
# Compute bin: bin is rounded to the nearest multiples of step
df["_bin"] = np.round(df["_step"] / step) * step
# Group by bin and average each metric
grouped = df.groupby("_bin", as_index=False)[metrics].mean()
# Ensure bins are sorted
grouped = grouped.sort_values("_bin").reset_index(drop=True)
return grouped
def build_chartjs(
per_run_metric_df: Dict[Tuple[str, str], pd.DataFrame],
label_format: str,
) -> Dict[str, Any]:
"""
Build a Chart.js line chart dataset:
labels: union of all bins across runs (sorted)
datasets: one per (run, metric) pair, aligned to labels, with None for missing points
"""
# Union of all bins
all_bins = set()
for df in per_run_metric_df.values():
all_bins.update(df["_bin"].tolist())
labels = sorted(all_bins)
# Chart.js wants arrays of primitive x labels (we'll use the bin starts)
# If you want to render actual x=_step values, labels are these bin starts.
datasets = []
for (run_name, metric), df in per_run_metric_df.items():
series_map = dict(zip(df["_bin"].tolist(), df[metric].tolist()))
data = [series_map.get(b, None) for b in labels]
datasets.append(
{
"label": label_format.format(run=run_name, metric=metric),
"data": data,
# Chart.js can infer styles; consumers can style further on the frontend
"spanGaps": True, # nicer lines across missing bins
}
)
return {
"type": "line",
"data": {
"labels": labels,
"datasets": datasets,
},
"options": {
"interaction": {"mode": "nearest", "intersect": False},
"plugins": {
"legend": {"display": True, "position": "top"},
"title": {"display": True, "text": "W&B Metrics (binned by step)"},
},
"scales": {
"x": {"title": {"display": True, "text": "Step (bin start)"}},
"y": {"title": {"display": True, "text": "Value"}},
},
},
}
def main():
args = parse_args()
api = wandb.Api()
entity = args.entity or api.default_entity
if not entity:
print("::error::Unable to determine W&B entity. Pass --entity.", file=sys.stderr)
sys.exit(1)
runs = fetch_runs(api, entity, args.project, args.runs)
missing = [r for r in args.runs if r not in runs]
if missing:
msg = f"Runs not found: {', '.join(missing)}"
if args.strict:
print(f"::error::{msg}", file=sys.stderr)
sys.exit(1)
else:
print(f"::warning::{msg}", file=sys.stderr)
if not runs:
print("::error::No matching runs found.", file=sys.stderr)
sys.exit(1)
per_run_metric_df: Dict[Tuple[str, str], pd.DataFrame] = {}
for run_name, run in runs.items():
# Fetch each metric separately to avoid losing sparse metrics due to row intersection.
for metric in args.metrics:
hist = run.history(keys=["_step", metric], pandas=True)
if hist is None or hist.empty:
msg = f"No history for run '{run_name}' (metric '{metric}')."
if args.strict:
print(f"::error::{msg}", file=sys.stderr)
sys.exit(1)
else:
print(f"::warning::{msg}", file=sys.stderr)
continue
# Ensure numeric _step
if "_step" not in hist.columns:
print(
f"::warning::Run '{run_name}' has no '_step' column; skipping metric '{metric}'.",
file=sys.stderr,
)
continue
# Clean to numeric where possible
hist["_step"] = pd.to_numeric(hist["_step"], errors="coerce")
hist = hist.dropna(subset=["_step"])
hist["_step"] = hist["_step"].astype(int)
# Aggregate per metric; dense metrics can be tamed with --step (e.g., 16)
grouped = aggregate_history(hist, [metric], args.step)
if metric not in grouped.columns:
msg = f"Metric '{metric}' not found in run '{run_name}'."
if args.strict:
print(f"::error::{msg}", file=sys.stderr)
sys.exit(1)
else:
print(f"::warning::{msg}", file=sys.stderr)
continue
# Keep only _bin and the single metric for simpler merging later
per_run_metric_df[(run_name, metric)] = grouped[["_bin", metric]].copy()
if not per_run_metric_df:
print("::error::No data collected for any run/metric.", file=sys.stderr)
sys.exit(1)
chart = build_chartjs(per_run_metric_df, args.label_format)
payload = json.dumps(chart, ensure_ascii=False)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(payload)
print(f"Wrote Chart.js JSON to: {args.out}")
else:
print(payload)
if __name__ == "__main__":
main()