Skip to content

Commit c4eddb4

Browse files
locus313Patrick Lewis
andauthored
Feature/copilot report ai credit budgets (#53)
* chore: ignore macOS .DS_Store files * feat: add enterprise AI credit budget reporting to github-copilot-report Reports each licensed user's effective Universal/Individual AI credit budget and consumption via the enterprise-level Budgets API (GET /enterprises/{enterprise}/settings/billing/budgets and .../budgets/{id}/user-states), adding Budget Scope/Limit/Used/Remaining CSV columns. Adds a --no-budgets flag to skip the lookup. --------- Co-authored-by: Patrick Lewis <plewis@ontinue.com>
1 parent 9ab9c06 commit c4eddb4

5 files changed

Lines changed: 127 additions & 4 deletions

File tree

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ output.txt
1111
# Python bytecode/cache
1212
**/__pycache__/
1313
**/*.pyc
14+
15+
# macOS
16+
.DS_Store
17+
**/.DS_Store

‎README.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ cd reporting/github-repo-permissions-report
575575

576576
**Script:** `reporting/github-copilot-report/github-copilot-report.sh`
577577

578-
Generates a GitHub Copilot Enterprise licence and usage report. Shows every licensed user, their plan type, pool credit contribution, and actual AI credit consumption for the current billing month. Optionally enriches data with Entra ID department information.
578+
Generates a GitHub Copilot Enterprise licence and usage report. Shows every licensed user, their plan type, pool credit contribution, and actual AI credit consumption for the current billing month, plus their effective AI credit budget (Individual override, else Universal) and how much of it has been consumed. Optionally enriches data with Entra ID department information.
579579

580580
**Prerequisites:**
581581
- **[curl](https://curl.se)** — HTTP client
@@ -606,6 +606,7 @@ az login # optional; needed only for Entra ID department enrichment
606606
./github-copilot-report.sh -e YOUR-ENTERPRISE
607607
./github-copilot-report.sh -e YOUR-ENTERPRISE -d example.com
608608
./github-copilot-report.sh -e YOUR-ENTERPRISE --no-entra
609+
./github-copilot-report.sh -e YOUR-ENTERPRISE --no-budgets
609610
./github-copilot-report.sh -e YOUR-ENTERPRISE --credits 1900 --output report.csv
610611
```
611612

@@ -618,10 +619,12 @@ az login # optional; needed only for Entra ID department enrichment
618619
| `--credits N` | Override credits-per-seat value (or `$CREDITS_PER_SEAT_OVERRIDE`) | Auto-detected |
619620
| `--output FILE` | Output CSV filename | `copilot-report-YYYYMMDD.csv` |
620621
| `--no-entra` | Skip Entra ID department lookup | — |
622+
| `--no-budgets` | Skip per-user AI credit budget lookup (Universal/Individual) | — |
621623

622624
**What it does:**
623625
- Fetches all Copilot seats across the enterprise (deduplicated by user)
624626
- Fetches per-user AI credit consumption for the current billing month
627+
- Fetches enterprise-level AI credit budgets (Universal and Individual overrides) and each user's consumed amount against them
625628
- Fetches enterprise-level model usage metrics (last 28 days)
626629
- Optionally enriches each user with Entra ID department and job title via `az rest`
627630
- Outputs a CSV and a formatted console summary with department breakdown and model usage tables
@@ -630,7 +633,7 @@ az login # optional; needed only for Entra ID department enrichment
630633
> Requires a PAT with `read:enterprise` and `manage_billing:enterprise` scopes — the built-in `GITHUB_TOKEN` cannot grant enterprise-level access. Set `GITHUB_TOKEN` before executing (or have an active `gh` auth session with those scopes so the lib can auto-resolve the token).
631634
632635
> [!NOTE]
633-
> The Entra ID enrichment is skipped automatically if `az` is not logged in, or can be disabled with `--no-entra`.
636+
> The Entra ID enrichment is skipped automatically if `az` is not logged in, or can be disabled with `--no-entra`. Budget lookups require the token owner to be an enterprise admin or billing manager, and can be disabled with `--no-budgets`.
634637
635638
---
636639

‎reporting/github-copilot-report/action.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ inputs:
2727
description: 'Skip Entra ID department lookup'
2828
required: false
2929
default: 'false'
30+
no-budgets:
31+
description: 'Skip per-user AI credit budget lookup (Universal/Individual)'
32+
required: false
33+
default: 'false'
3034
runs:
3135
using: composite
3236
steps:
@@ -42,4 +46,5 @@ runs:
4246
ARGS=()
4347
[[ -n "${{ inputs.output }}" ]] && ARGS+=(--output "${{ inputs.output }}")
4448
[[ "${{ inputs.no-entra }}" == "true" ]] && ARGS+=(--no-entra)
49+
[[ "${{ inputs.no-budgets }}" == "true" ]] && ARGS+=(--no-budgets)
4550
"${{ github.action_path }}/github-copilot-report.sh" "${ARGS[@]}"

‎reporting/github-copilot-report/github-copilot-report.sh‎

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
# • Every user with a Copilot seat, their plan type, and their pool contribution
1919
# (credits per assigned seat — credits are pooled at the enterprise level)
2020
# • Actual per-user AI credit consumption this month (from billing API)
21+
# • Each user's effective AI credit budget (Individual override, else Universal)
22+
# and how much of it has been consumed (from the Budgets API)
2123
# • Users grouped by Entra ID department
2224
# • Enterprise-level model usage breakdown
2325
#
@@ -30,6 +32,19 @@
3032
#
3133
# Requires API version 2026-03-10 and the new Copilot usage metrics endpoints.
3234
# The legacy /copilot/metrics and /copilot/usage endpoints were closed Apr 2, 2026.
35+
#
36+
# User-level AI credit budgets (GitHub Budgets API, enterprise-scoped):
37+
# GET /enterprises/{enterprise}/settings/billing/budgets
38+
# GET /enterprises/{enterprise}/settings/billing/budgets/{budget_id}/user-states
39+
# Universal budget (budget_scope=multi_user_customer) applies to every user
40+
# unless overridden.
41+
# Individual budget (budget_scope=user) overrides the Universal budget for
42+
# one specific user.
43+
# Both are configured via Enterprise → Policies → Copilot → Budgets → Users.
44+
# The user-states endpoint returns each user's effective target/consumed
45+
# amount plus an override_budget_id when an Individual budget applies.
46+
# Requires the token owner to be an enterprise admin or billing manager.
47+
# Use --no-budgets to skip.
3348
# =============================================================================
3449

3550
set -euo pipefail
@@ -47,6 +62,7 @@ ENTRA_TENANT="${ENTRA_TENANT:-}"
4762
CREDITS_PER_SEAT_OVERRIDE="${CREDITS_PER_SEAT_OVERRIDE:-}"
4863
OUTPUT_CSV="copilot-report-$(date +%Y%m%d).csv"
4964
NO_ENTRA=false
65+
NO_BUDGETS=false
5066
GRAPH_TOKEN=""
5167
ENTRA_ENABLED=false
5268

@@ -113,6 +129,7 @@ Options:
113129
Use if your portal shows a different pool size than expected
114130
--output FILE Output CSV (default: copilot-report-YYYYMMDD.csv)
115131
--no-entra Skip Entra ID department lookup
132+
--no-budgets Skip per-user AI credit budget lookup (Universal/Individual)
116133
-h, --help Show this message
117134
118135
Required GITHUB_TOKEN scopes:
@@ -133,6 +150,7 @@ while [[ $# -gt 0 ]]; do
133150
--credits) CREDITS_PER_SEAT_OVERRIDE="$2"; shift 2 ;;
134151
--output) OUTPUT_CSV="$2"; shift 2 ;;
135152
--no-entra) NO_ENTRA=true; shift ;;
153+
--no-budgets) NO_BUDGETS=true; shift ;;
136154
-h|--help) usage; exit 0 ;;
137155
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
138156
esac
@@ -368,6 +386,82 @@ else
368386
print_warning "Failed users:${_billing_fail_statuses}"
369387
fi
370388

389+
# ── Fetch enterprise AI credit budgets (Budgets API: Universal / Individual) ─
390+
# Two calls total (each paginated), not one per user:
391+
# 1. GET /enterprises/{enterprise}/settings/billing/budgets — locates the
392+
# Universal (multi_user_customer) budget and any direct Individual
393+
# (user-scope) budgets.
394+
# 2. GET .../budgets/{universal_id}/user-states — per-user consumed/target
395+
# amounts against the Universal budget; override_budget_id marks users
396+
# who have an Individual override.
397+
declare -A USER_BUDGET_SCOPE=() # "Individual" | "Universal" | "None"
398+
declare -A USER_BUDGET_AMOUNT=() # USD
399+
declare -A USER_BUDGET_USED=() # USD
400+
401+
if [[ "$NO_BUDGETS" == "true" ]]; then
402+
print_warning "AI credit budget lookup disabled (--no-budgets)."
403+
else
404+
print_status "Fetching enterprise AI credit budgets (Universal / Individual)..."
405+
406+
_all_budgets="[]"
407+
_page=1
408+
while :; do
409+
_bresp=$(gh_api "/enterprises/${GITHUB_ENTERPRISE}/settings/billing/budgets?per_page=100&page=${_page}" --api-version 2026-03-10) || _bresp=""
410+
[[ "${_bresp}" == "__404__" || "${_bresp}" == "__422__" || -z "$_bresp" ]] && break
411+
_all_budgets=$(jq -cn --argjson a "$_all_budgets" --argjson b "$(echo "$_bresp" | jq -c '.budgets // []')" '$a + $b')
412+
[[ "$(echo "$_bresp" | jq -r '.has_next_page // false')" == "true" ]] || break
413+
_page=$(( _page + 1 ))
414+
done
415+
416+
_ai_budgets=$(echo "$_all_budgets" | jq -c '[.[] | select(.budget_product_sku=="ai_credits")]')
417+
418+
# Direct Individual (user-scope) budgets — covers the case where no Universal budget exists.
419+
while IFS=$'\t' read -r _u _amt _used; do
420+
[[ -z "$_u" ]] && continue
421+
USER_BUDGET_SCOPE["$_u"]="Individual"
422+
USER_BUDGET_AMOUNT["$_u"]="$_amt"
423+
USER_BUDGET_USED["$_u"]="$_used"
424+
done < <(echo "$_ai_budgets" | jq -r '.[] | select(.budget_scope=="user") | [.user, .budget_amount, (.effective_budget.consumed_amount // .consumed_amount // 0)] | @tsv')
425+
426+
_universal_id=$(echo "$_ai_budgets" | jq -r '[.[] | select(.budget_scope=="multi_user_customer")][0].id // empty')
427+
_universal_amount=$(echo "$_ai_budgets" | jq -r '[.[] | select(.budget_scope=="multi_user_customer")][0].budget_amount // empty')
428+
429+
if [[ -n "$_universal_id" ]]; then
430+
_page=1
431+
while :; do
432+
_sresp=$(gh_api "/enterprises/${GITHUB_ENTERPRISE}/settings/billing/budgets/${_universal_id}/user-states?per_page=100&page=${_page}" --api-version 2026-03-10) || _sresp=""
433+
[[ "${_sresp}" == "__404__" || "${_sresp}" == "__422__" || -z "$_sresp" ]] && break
434+
while IFS=$'\t' read -r _u _consumed _target _override; do
435+
[[ -z "$_u" ]] && continue
436+
if [[ -n "$_override" && "$_override" != "null" ]]; then
437+
USER_BUDGET_SCOPE["$_u"]="Individual"
438+
else
439+
USER_BUDGET_SCOPE["$_u"]="Universal"
440+
fi
441+
USER_BUDGET_AMOUNT["$_u"]="$_target"
442+
USER_BUDGET_USED["$_u"]="$_consumed"
443+
done < <(echo "$_sresp" | jq -r '.user_states[]? | [.user, .consumed_amount, .target_amount, (.override_budget_id // "")] | @tsv')
444+
[[ "$(echo "$_sresp" | jq -r '.has_next_page // false')" == "true" ]] || break
445+
_page=$(( _page + 1 ))
446+
done
447+
448+
# Licensed users covered by the Universal budget but absent from user-states (no usage yet)
449+
while IFS= read -r _login; do
450+
[[ -z "$_login" ]] && continue
451+
[[ -n "${USER_BUDGET_SCOPE[$_login]+_}" ]] && continue
452+
USER_BUDGET_SCOPE["$_login"]="Universal"
453+
USER_BUDGET_AMOUNT["$_login"]="$_universal_amount"
454+
USER_BUDGET_USED["$_login"]="0"
455+
done <<< "$_ALL_LOGINS"
456+
fi
457+
458+
if [[ "${#USER_BUDGET_SCOPE[@]}" -gt 0 ]]; then
459+
print_status "Loaded budget data for ${#USER_BUDGET_SCOPE[@]} user(s)."
460+
else
461+
print_warning "No enterprise AI credit budgets are configured (Enterprise → Policies → Copilot → Budgets), or the token lacks enterprise admin/billing manager access."
462+
fi
463+
fi
464+
371465
# Build sorted list of all model names seen across all users
372466
ALL_MODELS=()
373467
while IFS= read -r _m; do
@@ -400,7 +494,7 @@ fi
400494
# ── Write CSV header ──────────────────────────────────────────────────────────
401495
_model_header_cols=$(printf ',"Credits: %s"' "${ALL_MODELS[@]}")
402496
printf '%s\n' \
403-
"GitHub Login,Display Name,Email / UPN,Organisation,Plan Type,Pool Contribution (Credits/Seat),AI Credits Used (Month)${_model_header_cols},Department,Job Title,Last Active,Last Editor" \
497+
"GitHub Login,Display Name,Email / UPN,Organisation,Plan Type,Pool Contribution (Credits/Seat),AI Credits Used (Month),Budget Scope,Budget Limit (USD),Budget Used (USD),Budget Remaining (USD)${_model_header_cols},Department,Job Title,Last Active,Last Editor" \
404498
> "$OUTPUT_CSV"
405499

406500
# ── Per-department counters ───────────────────────────────────────────────────
@@ -423,6 +517,13 @@ while IFS= read -r seat; do
423517
credits=$(plan_credits "$plan")
424518
credits_used="${USER_CREDITS_USED[$login]:-}"
425519

520+
budget_scope="${USER_BUDGET_SCOPE[$login]:-None}"
521+
budget_amount="${USER_BUDGET_AMOUNT[$login]:-}"
522+
budget_used="${USER_BUDGET_USED[$login]:-}"
523+
budget_remaining=""
524+
[[ "$budget_scope" != "None" ]] && \
525+
budget_remaining=$(awk -v a="${budget_amount:-0}" -v u="${budget_used:-0}" 'BEGIN{printf "%.2f", a-u}')
526+
426527
# Prefer email; fall back to UPN derived from login + --upn-domain
427528
if [[ -n "$email" ]]; then
428529
lookup="$email"
@@ -439,14 +540,18 @@ while IFS= read -r seat; do
439540
[[ -z "$disp" ]] && disp="$gh_name"
440541

441542
# Append to CSV — double-quote all fields, escape embedded quotes
442-
printf '"%s","%s","%s","%s","%s","%s","%s"' \
543+
printf '"%s","%s","%s","%s","%s","%s","%s","%s","%s","%s","%s"' \
443544
"${login//\"/\"\"}" \
444545
"${disp//\"/\"\"}" \
445546
"${lookup//\"/\"\"}" \
446547
"${org//\"/\"\"}" \
447548
"${plan//\"/\"\"}" \
448549
"$credits" \
449550
"${credits_used}" \
551+
"${budget_scope}" \
552+
"${budget_amount}" \
553+
"${budget_used}" \
554+
"${budget_remaining}" \
450555
>> "$OUTPUT_CSV"
451556
# Per-model credit columns (0 if user had no usage on that model)
452557
for _m in "${ALL_MODELS[@]}"; do

‎tests/test_script_validation.bats‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,12 @@ _run_script() {
486486
[ "$status" -eq 0 ]
487487
}
488488

489+
@test "github-copilot-report: --no-budgets is recognised (fails at token check)" {
490+
_run_script "${REPO_ROOT}/reporting/github-copilot-report/github-copilot-report.sh" "unset GITHUB_TOKEN;" "--no-budgets"
491+
[ "$status" -eq 1 ]
492+
[[ "$output" != *"Unknown option"* ]]
493+
}
494+
489495
# ═══════════════════════════════════════════════════════════════════════════════
490496
# personal/github-organize-stars
491497
# ═══════════════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)