Skip to content

Commit b601f34

Browse files
authored
Merge pull request #1565 from syedriko/syedriko-ols-1958
LCORE-1958: Correct OpenAPI spec violations in docs/openapi.json, add OpenAPI linting to CI
2 parents 8255cb1 + c2b7827 commit b601f34

11 files changed

Lines changed: 384 additions & 17 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# OpenAPI: regenerate check (Spectral + committed docs/openapi.json drift).
2+
# - scripts/generate_openapi_schema.py builds the spec from FastAPI (app.main).
3+
# - CI fails if docs/openapi.json does not match generator output (run locally:
4+
# uv run scripts/generate_openapi_schema.py docs/openapi.json).
5+
name: OpenAPI (Spectral)
6+
7+
on:
8+
- push
9+
- pull_request
10+
11+
jobs:
12+
spectral:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
pull-requests: read
17+
steps:
18+
- uses: actions/checkout@v4
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v5
21+
with:
22+
python-version: "3.12"
23+
- name: Install dependencies
24+
# Same pattern as local dev (CONTRIBUTING.md): dev + llslibdev for a full app import.
25+
run: uv sync --group dev --group llslibdev
26+
- name: Install PDM
27+
# scripts/generate_openapi_schema.py asserts OpenAPI info.version matches `pdm show --version`.
28+
run: uv pip install pdm
29+
- name: Verify docs/openapi.json matches generator
30+
run: |
31+
set -euo pipefail
32+
uv run python scripts/generate_openapi_schema.py /tmp/openapi-generated.json
33+
if ! diff -u docs/openapi.json /tmp/openapi-generated.json; then
34+
echo "::error::docs/openapi.json is out of date. Regenerate with: uv run scripts/generate_openapi_schema.py docs/openapi.json"
35+
exit 1
36+
fi
37+
- name: Setup Node.js
38+
uses: actions/setup-node@v4
39+
with:
40+
node-version: "22"
41+
- name: Spectral lint
42+
run: npx --yes @stoplight/spectral-cli@6 lint docs/openapi.json --fail-severity error --display-only-failures

.spectral.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Spectral (OpenAPI) — https://meta.stoplight.io/docs/spectral/
2+
#
3+
# Full Stoplight OAS ruleset. `oas3-valid-media-example` is off: examples in
4+
# docs/openapi.json are often partial and produced ~600 warnings with little
5+
# signal until examples are aligned with schemas. Re-enable when tightening
6+
# examples (set to "warn" or "error").
7+
extends: spectral:oas
8+
rules:
9+
oas3-valid-media-example: off

CONTRIBUTING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* [Pre-commit hook settings](#pre-commit-hook-settings)
1919
* [Code coverage measurement](#code-coverage-measurement)
2020
* [Linters](#linters)
21+
* [OpenAPI (Spectral)](#openapi-spectral)
2122
* [Type hints checks](#type-hints-checks)
2223
* [Ruff](#ruff)
2324
* [Pylint](#pylint)
@@ -48,6 +49,7 @@
4849
- git
4950
- Python 3.12 or 3.13
5051
- pip
52+
- **Node.js** (18 or newer; **npm** and **`npx`** ship with Node). **`make verify`** runs **`lint-openapi`**, which calls **`npx --yes @stoplight/spectral-cli@6`** (see `Makefile`). If **`npx`** is not installed, **`lint-openapi` is skipped** with a message so **`make verify` still succeeds** locally; install Node to run the OpenAPI check. **CI** always runs Spectral (see `.github/workflows/openapi_spectral.yaml`).
5153

5254
The development requires at least [Python 3.12](https://docs.python.org/3/whatsnew/3.12.html) due to significant improvement on performance, optimizations which benefit modern ML, AI, LLM, NL stacks, and improved asynchronous processing capabilities. It is also possible to use Python 3.13.
5355

@@ -57,6 +59,7 @@ The development requires at least [Python 3.12](https://docs.python.org/3/whatsn
5759

5860
1. `pip install --user uv`
5961
1. `uv --version` -- should return no error
62+
1. Install [Node.js](https://nodejs.org/en/download) (LTS is fine) or use your OS package manager, e.g. Fedora: `sudo dnf install nodejs`, macOS with [Homebrew](https://brew.sh/): `brew install node`. Confirm `node --version` and `npx --version` work. CI uses Node 22 for Spectral (see `.github/workflows/openapi_spectral.yaml`).
6063

6164

6265

@@ -217,6 +220,10 @@ Code coverage reports are generated in JSON and also in format compatible with [
217220

218221
_Black_, _Ruff_, Pyright, _Pylint_, __Pydocstyle__, __Mypy__, and __Bandit__ tools are used as linters. There are a bunch of linter rules enabled for this repository. All of them are specified in `pyproject.toml`, such as in sections `[tool.ruff]` and `[tool.pylint."MESSAGES CONTROL"]`. Some specific rules can be disabled using `ignore` parameter (empty now).
219222

223+
### OpenAPI (Spectral)
224+
225+
OpenAPI is linted with [Spectral](https://stoplight.io/open-api/) via **`npx --yes @stoplight/spectral-cli@6`** in the **`lint-openapi`** target (`make lint-openapi`, part of **`make verify`**). If **`npx`** is missing, **`lint-openapi`** skips Spectral locally; install **Node.js** to run it (see [Prerequisites](#prerequisites) and [Tooling installation](#tooling-installation)). **CI** always runs the Spectral step. If you introduce a **new** router tag (`APIRouter(tags=[...])`), you must also extend the global tag list in `src/app/main.py` and regenerate `docs/openapi.json`. See **[docs/contributing/openapi-tags-and-spectral.md](docs/contributing/openapi-tags-and-spectral.md)**.
226+
220227

221228
### Type hints checks
222229

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,21 @@ docstyle: ## Check the docstring style using Docstyle checker
113113
ruff: ## Check source code using Ruff linter
114114
uv run ruff check src tests --per-file-ignores=tests/*:S101 --per-file-ignores=scripts/*:S101
115115

116+
lint-openapi: ## Lint docs/openapi.json (Spectral OAS ruleset; fail on error)
117+
@if command -v npx >/dev/null 2>&1; then \
118+
npx --yes @stoplight/spectral-cli@6 lint docs/openapi.json --fail-severity error --display-only-failures; \
119+
else \
120+
echo "lint-openapi: skipping Spectral (npx not found). Install Node.js for OpenAPI lint locally; CI still runs it."; \
121+
fi
122+
116123
verify: ## Run all linters
117124
$(MAKE) black
118125
$(MAKE) pylint
119126
$(MAKE) pyright
120127
$(MAKE) ruff
121128
$(MAKE) docstyle
122129
$(MAKE) check-types
130+
$(MAKE) lint-openapi
123131

124132
distribution-archives: ## Generate distribution archives to be uploaded into Python registry
125133
rm -rf dist
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# OpenAPI tags and Spectral
2+
3+
## Global tag list (`_OPENAPI_TAGS`)
4+
5+
In `src/app/endpoints/`, route tags come from **`APIRouter(tags=[...])`**, which FastAPI uses when it builds the OpenAPI description for each operation.
6+
7+
The OpenAPI document must list those tags at the top level for tools like [Spectral](https://stoplight.io/open-api/) rule **`operation-tag-defined`** to pass, so we keep **`_OPENAPI_TAGS`** in **`src/app/main.py`** and pass it into the **`FastAPI`** app as **`openapi_tags`**.
8+
9+
**When you add a new router or change `tags=[...]` to use a new tag name**, add a matching entry to **`_OPENAPI_TAGS`** (same `name` string, plus a short `description` for the docs).
10+
11+
The schema generator **`scripts/generate_openapi_schema.py`** passes **`tags=app.openapi_tags`** into **`get_openapi()`** so **`docs/openapi.json`** includes the top-level `tags` array. Regenerate after tag changes:
12+
13+
```bash
14+
uv run scripts/generate_openapi_schema.py docs/openapi.json
15+
```
16+
17+
## Linting (`make lint-openapi`)
18+
19+
Spectral is configured in **`.spectral.yaml`** (extends `spectral:oas`). Run:
20+
21+
```bash
22+
make lint-openapi
23+
```
24+
25+
This is part of **`make verify`**. If **`npx`** is not on your **`PATH`**, the Makefile **skips** Spectral and prints a short message so **`make verify`** can still pass; install Node.js to run the check locally. **CI** (`.github/workflows/openapi_spectral.yaml`) always runs Spectral. Failures are driven by **error**-severity rules.
26+
27+
The rule **`oas3-valid-media-example`** (examples must match schemas) is **turned off** in **`.spectral.yaml`** because the generated spec carries many partial examples and produced hundreds of noisy warnings. Turn it back on when examples are brought in line with schemas.

docs/openapi.json

Lines changed: 142 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
"servers": [
1919
{
20-
"url": "http://localhost:8080/",
20+
"url": "http://localhost:8080",
2121
"description": "Locally running service"
2222
}
2323
],
@@ -9262,8 +9262,7 @@
92629262
"schema": {
92639263
"type": "string"
92649264
},
9265-
"example": "event: response.created\ndata: {\"type\":\"response.created\",\"sequence_number\":0,\"response\":{\"id\":\"resp_abc\",\"object\":\"response\",\"created_at\":1704067200,\"status\":\"in_progress\",\"model\":\"openai/gpt-4o-mini\",\"output\":[],\"store\":true,\"text\":{\"format\":{\"type\":\"text\"}},\"conversation\":\"0d21ba731f21f798dc9680125d5d6f49\",\"available_quotas\":{},\"output_text\":\"\"}}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"sequence_number\":1,\"response_id\":\"resp_abc\",\"output_index\":0,\"item\":{\"id\":\"msg_abc\",\"type\":\"message\",\"status\":\"in_progress\",\"role\":\"assistant\",\"content\":[]}}\n\n...\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"sequence_number\":30,\"response\":{\"id\":\"resp_abc\",\"object\":\"response\",\"created_at\":1704067200,\"status\":\"completed\",\"model\":\"openai/gpt-4o-mini\",\"output\":[{\"id\":\"msg_abc\",\"type\":\"message\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello! How can I help?\",\"annotations\":[]}]}],\"store\":true,\"text\":{\"format\":{\"type\":\"text\"}},\"usage\":{\"input_tokens\":10,\"output_tokens\":6,\"total_tokens\":16,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens_details\":{\"reasoning_tokens\":0}},\"conversation\":\"0d21ba731f21f798dc9680125d5d6f49\",\"available_quotas\":{\"daily\":1000,\"monthly\":50000},\"output_text\":\"Hello! How can I help?\"}}\n\ndata: [DONE]\n\n",
9266-
"description": "SSE stream of events"
9265+
"example": "event: response.created\ndata: {\"type\":\"response.created\",\"sequence_number\":0,\"response\":{\"id\":\"resp_abc\",\"object\":\"response\",\"created_at\":1704067200,\"status\":\"in_progress\",\"model\":\"openai/gpt-4o-mini\",\"output\":[],\"store\":true,\"text\":{\"format\":{\"type\":\"text\"}},\"conversation\":\"0d21ba731f21f798dc9680125d5d6f49\",\"available_quotas\":{},\"output_text\":\"\"}}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"sequence_number\":1,\"response_id\":\"resp_abc\",\"output_index\":0,\"item\":{\"id\":\"msg_abc\",\"type\":\"message\",\"status\":\"in_progress\",\"role\":\"assistant\",\"content\":[]}}\n\n...\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"sequence_number\":30,\"response\":{\"id\":\"resp_abc\",\"object\":\"response\",\"created_at\":1704067200,\"status\":\"completed\",\"model\":\"openai/gpt-4o-mini\",\"output\":[{\"id\":\"msg_abc\",\"type\":\"message\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello! How can I help?\",\"annotations\":[]}]}],\"store\":true,\"text\":{\"format\":{\"type\":\"text\"}},\"usage\":{\"input_tokens\":10,\"output_tokens\":6,\"total_tokens\":16,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens_details\":{\"reasoning_tokens\":0}},\"conversation\":\"0d21ba731f21f798dc9680125d5d6f49\",\"available_quotas\":{\"daily\":1000,\"monthly\":50000},\"output_text\":\"Hello! How can I help?\"}}\n\ndata: [DONE]\n\n"
92679266
}
92689267
}
92699268
},
@@ -10600,15 +10599,31 @@
1060010599
"tags": [
1060110600
"a2a"
1060210601
],
10603-
"summary": "Handle A2A Jsonrpc",
10604-
"description": "Handle A2A JSON-RPC requests following the A2A protocol specification.\n\nThis endpoint uses the DefaultRequestHandler from the A2A SDK to handle\nall JSON-RPC requests including message/send, message/stream, etc.\n\nThe A2A SDK application is created per-request to include authentication\ncontext while still leveraging FastAPI's authorization middleware.\n\nAutomatically detects streaming requests (message/stream JSON-RPC method)\nand returns a StreamingResponse to enable real-time chunk delivery.\n\nArgs:\n request: FastAPI request object\n auth: Authentication tuple\n mcp_headers: MCP headers for context propagation\n\nReturns:\n JSON-RPC response or streaming response",
10602+
"summary": "Handle A2A JSON-RPC GET",
10603+
"description": "Handle GET on /a2a for A2A JSON-RPC requests following the A2A protocol specification.",
1060510604
"operationId": "handle_a2a_jsonrpc_a2a_get",
1060610605
"responses": {
1060710606
"200": {
10608-
"description": "Successful Response",
10607+
"description": "Successful response",
1060910608
"content": {
1061010609
"application/json": {
10611-
"schema": {}
10610+
"schema": {
10611+
"type": "object",
10612+
"description": "JSON-RPC 2.0 response or A2A-over-HTTP payload"
10613+
},
10614+
"example": {
10615+
"jsonrpc": "2.0",
10616+
"id": "1",
10617+
"result": {}
10618+
}
10619+
},
10620+
"text/event-stream": {
10621+
"schema": {
10622+
"type": "string",
10623+
"format": "text/event-stream",
10624+
"description": "Server-Sent Events stream when the JSON-RPC method is message/stream"
10625+
},
10626+
"example": "data: {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{}}\n\n"
1061210627
}
1061310628
}
1061410629
}
@@ -10618,15 +10633,31 @@
1061810633
"tags": [
1061910634
"a2a"
1062010635
],
10621-
"summary": "Handle A2A Jsonrpc",
10622-
"description": "Handle A2A JSON-RPC requests following the A2A protocol specification.\n\nThis endpoint uses the DefaultRequestHandler from the A2A SDK to handle\nall JSON-RPC requests including message/send, message/stream, etc.\n\nThe A2A SDK application is created per-request to include authentication\ncontext while still leveraging FastAPI's authorization middleware.\n\nAutomatically detects streaming requests (message/stream JSON-RPC method)\nand returns a StreamingResponse to enable real-time chunk delivery.\n\nArgs:\n request: FastAPI request object\n auth: Authentication tuple\n mcp_headers: MCP headers for context propagation\n\nReturns:\n JSON-RPC response or streaming response",
10623-
"operationId": "handle_a2a_jsonrpc_a2a_get",
10636+
"summary": "Handle A2A JSON-RPC POST",
10637+
"description": "Handle POST on /a2a for A2A JSON-RPC requests following the A2A protocol specification.",
10638+
"operationId": "handle_a2a_jsonrpc_a2a_post",
1062410639
"responses": {
1062510640
"200": {
10626-
"description": "Successful Response",
10641+
"description": "Successful response",
1062710642
"content": {
1062810643
"application/json": {
10629-
"schema": {}
10644+
"schema": {
10645+
"type": "object",
10646+
"description": "JSON-RPC 2.0 response or A2A-over-HTTP payload"
10647+
},
10648+
"example": {
10649+
"jsonrpc": "2.0",
10650+
"id": "1",
10651+
"result": {}
10652+
}
10653+
},
10654+
"text/event-stream": {
10655+
"schema": {
10656+
"type": "string",
10657+
"format": "text/event-stream",
10658+
"description": "Server-Sent Events stream when the JSON-RPC method is message/stream"
10659+
},
10660+
"example": "data: {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{}}\n\n"
1063010661
}
1063110662
}
1063210663
}
@@ -20478,5 +20509,103 @@
2047820509
]
2047920510
}
2048020511
}
20481-
}
20512+
},
20513+
"tags": [
20514+
{
20515+
"name": "a2a",
20516+
"description": "Agent-to-Agent (A2A) protocol."
20517+
},
20518+
{
20519+
"name": "authorized",
20520+
"description": "Authorization probe."
20521+
},
20522+
{
20523+
"name": "config",
20524+
"description": "Service configuration."
20525+
},
20526+
{
20527+
"name": "conversations_v1",
20528+
"description": "Conversations API v1."
20529+
},
20530+
{
20531+
"name": "conversations_v2",
20532+
"description": "Conversations API v2."
20533+
},
20534+
{
20535+
"name": "feedback",
20536+
"description": "User feedback."
20537+
},
20538+
{
20539+
"name": "health",
20540+
"description": "Health and readiness probes."
20541+
},
20542+
{
20543+
"name": "info",
20544+
"description": "Service information."
20545+
},
20546+
{
20547+
"name": "mcp-auth",
20548+
"description": "MCP client authentication options."
20549+
},
20550+
{
20551+
"name": "mcp-servers",
20552+
"description": "MCP server registration."
20553+
},
20554+
{
20555+
"name": "metrics",
20556+
"description": "Prometheus metrics."
20557+
},
20558+
{
20559+
"name": "models",
20560+
"description": "LLM models."
20561+
},
20562+
{
20563+
"name": "prompts",
20564+
"description": "Prompt management."
20565+
},
20566+
{
20567+
"name": "providers",
20568+
"description": "Inference providers."
20569+
},
20570+
{
20571+
"name": "query",
20572+
"description": "Non-streaming query."
20573+
},
20574+
{
20575+
"name": "rags",
20576+
"description": "RAG configuration."
20577+
},
20578+
{
20579+
"name": "responses",
20580+
"description": "OpenAI-compatible Responses API."
20581+
},
20582+
{
20583+
"name": "rlsapi-v1",
20584+
"description": "RLS API v1 (inference)."
20585+
},
20586+
{
20587+
"name": "root",
20588+
"description": "Service root."
20589+
},
20590+
{
20591+
"name": "shields",
20592+
"description": "Safety shields."
20593+
},
20594+
{
20595+
"name": "streaming_query",
20596+
"description": "Streaming query (SSE)."
20597+
},
20598+
{
20599+
"name": "streaming_query_interrupt",
20600+
"description": "Streaming interrupt."
20601+
},
20602+
{
20603+
"name": "tools",
20604+
"description": "Tools."
20605+
},
20606+
{
20607+
"name": "vector-stores",
20608+
"description": "Vector stores and files."
20609+
}
20610+
]
2048220611
}

scripts/generate_openapi_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def read_version_from_pyproject():
8585
license_info=app.license_info,
8686
servers=app.servers,
8787
contact=app.contact,
88+
tags=app.openapi_tags,
8889
)
8990

9091
# dump the schema into file

0 commit comments

Comments
 (0)