ci: commit openapi.json and gate it with a verify-codegen script

Adds the proxy's OpenAPI spec to the repo as openapi.json so diff tooling has a
committed baseline, and replaces the inline drift check with the update/verify
script pair Kubernetes uses so the same command runs locally and in CI.

- scripts/update-codegen.sh is the single writer for generated artifacts
- scripts/verify-codegen.sh runs it and fails on any git status change
- the sync job now runs on every PR instead of a path filter, so routes defined
  outside litellm/proxy (enterprise/**) can no longer land a stale spec
- git status --porcelain replaces git diff, which was blind to untracked files
This commit is contained in:
ryan-crabbe-berri 2026-08-07 15:51:35 -07:00
parent f6587faef5
commit cca1b8f06e
7 changed files with 89765 additions and 45 deletions

3
.gitattributes vendored
View file

@ -1,2 +1,3 @@
*.ipynb linguist-vendored
ui/litellm-dashboard/src/lib/http/schema.d.ts linguist-generated
ui/litellm-dashboard/src/lib/http/schema.d.ts linguist-generated
openapi.json linguist-generated

View file

@ -2,21 +2,13 @@ name: Check UI API Types Sync
on:
pull_request:
paths:
- "litellm/proxy/**"
- "litellm/types/**"
- "ui/litellm-dashboard/src/lib/http/schema.d.ts"
- "ui/litellm-dashboard/scripts/gen-api-types.mjs"
- "ui/litellm-dashboard/package.json"
- "ui/litellm-dashboard/package-lock.json"
- ".github/workflows/check-ui-api-types.yml"
permissions:
contents: read
jobs:
check-sync:
name: Verify schema.d.ts matches the proxy OpenAPI spec
name: Verify openapi.json and schema.d.ts match the proxy
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
@ -64,21 +56,7 @@ jobs:
working-directory: ui/litellm-dashboard
run: npm ci
- name: Regenerate types from the live spec
working-directory: ui/litellm-dashboard
- name: Verify generated files are up to date
env:
LITELLM_PYTHON: "uv run --no-sync python"
run: npm run gen:api
- name: Fail if types are stale
run: |
if ! git diff --exit-code -- ui/litellm-dashboard/src/lib/http/schema.d.ts; then
echo "::error file=ui/litellm-dashboard/src/lib/http/schema.d.ts::Generated API types are out of sync with the proxy OpenAPI spec."
echo ""
echo "A backend route or model changed without regenerating the dashboard types."
echo "To fix, run from ui/litellm-dashboard:"
echo " npm run gen:api"
echo "then commit the updated src/lib/http/schema.d.ts."
exit 1
fi
echo "schema.d.ts is in sync with the proxy OpenAPI spec."
run: ./scripts/verify-codegen.sh

89686
openapi.json generated Normal file

File diff suppressed because one or more lines are too long

20
scripts/update-codegen.sh Executable file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
DASHBOARD_DIR="${REPO_ROOT}/ui/litellm-dashboard"
if [[ -z "${LITELLM_PYTHON:-}" && -x "${REPO_ROOT}/.venv/bin/python" ]]; then
export LITELLM_PYTHON="${REPO_ROOT}/.venv/bin/python"
fi
if [[ ! -x "${DASHBOARD_DIR}/node_modules/.bin/openapi-typescript" ]]; then
echo "Installing dashboard dependencies..." >&2
npm ci --prefix "${DASHBOARD_DIR}"
fi
cd "${DASHBOARD_DIR}"
npm run gen:api

38
scripts/verify-codegen.sh Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "${REPO_ROOT}"
GENERATED_PATHS=(
"openapi.json"
"ui/litellm-dashboard/src/lib/http/schema.d.ts"
)
"${REPO_ROOT}/scripts/update-codegen.sh"
if [[ -z "$(git status --porcelain -- "${GENERATED_PATHS[@]}")" ]]; then
echo "Generated files are in sync with the proxy."
exit 0
fi
if [[ -n "${GITHUB_ACTIONS:-}" ]]; then
echo "::error file=openapi.json::Generated files are out of sync with the proxy. Run scripts/update-codegen.sh and commit the result."
fi
{
echo ""
echo "Generated files are out of sync with the proxy."
echo "A backend route or response model changed without regenerating them."
echo ""
git status --short -- "${GENERATED_PATHS[@]}"
git diff --stat -- "${GENERATED_PATHS[@]}"
echo ""
echo "The regenerated files are already in your working tree. Review and commit them,"
echo "or run scripts/update-codegen.sh yourself to reproduce this."
} >&2
exit 1

View file

@ -2,7 +2,7 @@ Never put LiteLLM tokens or API keys in `localStorage`. `localStorage` survives
When you fix lint violations that are grandfathered in `eslint-suppressions.json`, run `eslint . --prune-suppressions` and commit the updated baseline so the gate ratchets down instead of leaving a stale suppression
`src/lib/http/schema.d.ts` is generated from the proxy's OpenAPI spec; never hand-edit it. After changing a backend route or response model that the dashboard consumes, run `npm run gen:api` and commit the result (CI `Check UI API Types Sync` enforces this)
`openapi.json` at the repo root and `src/lib/http/schema.d.ts` are both generated from the proxy's FastAPI app; never hand-edit either. After changing any backend route or response model, run `scripts/update-codegen.sh` from the repo root and commit both files. `scripts/verify-codegen.sh` is the same generator plus a `git status` check, and it runs on every PR as `Check UI API Types Sync`, so you can reproduce a CI failure locally with it
Tests come in three tiers, named by the standard definitions. `Foo.test.tsx` is a unit test: one module, collaborators replaced by doubles, no multi-component tree, and it should run in milliseconds. `Foo.integration.test.tsx` renders a real component tree with real children and only stubs the network boundary; it costs seconds per case, so it earns its place by proving wiring that a unit test cannot reach. Browser-level tests live in `tests/e2e/ui/` as Playwright specs against a live proxy

View file

@ -1,5 +1,5 @@
/**
* Regenerates src/lib/http/schema.d.ts from the proxy's OpenAPI spec.
* Regenerates openapi.json and src/lib/http/schema.d.ts from the proxy's spec.
*
* Two hops, because the backend is the source of truth: the FastAPI app emits
* the spec from its route decorators (app.openapi()), then openapi-typescript
@ -7,20 +7,21 @@
* the spec is read straight off the app object, so this runs in CI without a
* database or proxy boot.
*
* Both outputs are committed. openapi.json is the checked-in API contract that
* diff tooling compares a branch against, and it is indented so a changed route
* shows up as readable lines in review rather than one rewritten blob.
*
* The Python interpreter must have litellm installed. Override which one via
* LITELLM_PYTHON (CI passes "uv run --no-sync python"); defaults to python3.
*/
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const dashboardDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolve(dashboardDir, "..", "..");
const outPath = join(dashboardDir, "src", "lib", "http", "schema.d.ts");
const specDir = mkdtempSync(join(tmpdir(), "litellm-openapi-"));
const specPath = join(specDir, "openapi.json");
const specPath = join(repoRoot, "openapi.json");
const python = (process.env.LITELLM_PYTHON ?? "python3").split(" ");
// The dashboard calls internal UI routes that the public /openapi.json hides via
@ -45,19 +46,15 @@ const dumpSpec = [
" if isinstance(node, list):",
" return [normalize(v) for v in node]",
" return node",
"with open(sys.argv[1], 'w') as f: json.dump(normalize(app.openapi()), f, sort_keys=True)",
"with open(sys.argv[1], 'w') as f: json.dump(normalize(app.openapi()), f, sort_keys=True, indent=2); f.write('\\n')",
].join("\n");
try {
execFileSync(python[0], [...python.slice(1), "-c", dumpSpec, specPath], {
cwd: repoRoot,
stdio: "inherit",
});
execFileSync(python[0], [...python.slice(1), "-c", dumpSpec, specPath], {
cwd: repoRoot,
stdio: "inherit",
});
execFileSync(join(dashboardDir, "node_modules", ".bin", "openapi-typescript"), [specPath, "-o", outPath], {
cwd: dashboardDir,
stdio: "inherit",
});
} finally {
rmSync(specDir, { recursive: true, force: true });
}
execFileSync(join(dashboardDir, "node_modules", ".bin", "openapi-typescript"), [specPath, "-o", outPath], {
cwd: dashboardDir,
stdio: "inherit",
});