Skip to content

Commit a0ad26f

Browse files
committed
LCORE-1958: Declare global OpenAPI tags for Spectral operation-tag-defined
Add _OPENAPI_TAGS / openapi_tags on FastAPI, pass tags through scripts/generate_openapi_schema.py, and regenerate docs/openapi.json with a root-level tags array and normalized server URL. Document tag maintenance in docs/contributing/openapi-tags-and-spectral.md (tag list section).
1 parent aee370b commit a0ad26f

4 files changed

Lines changed: 154 additions & 8 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
```

docs/openapi.json

Lines changed: 107 additions & 7 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
],
@@ -9205,7 +9205,7 @@
92059205
},
92069206
"responses": {
92079207
"200": {
9208-
"description": "Successful response. For `text/event-stream`, the body is a Server-Sent Events stream.",
9208+
"description": "Successful response",
92099209
"content": {
92109210
"application/json": {
92119211
"schema": {
@@ -10604,7 +10604,7 @@
1060410604
"operationId": "handle_a2a_jsonrpc_a2a_get",
1060510605
"responses": {
1060610606
"200": {
10607-
"description": "Successful response: buffered JSON-RPC or HTTP payload for non-streaming calls, or a Server-Sent Events stream when the JSON-RPC method is message/stream.",
10607+
"description": "Successful response",
1060810608
"content": {
1060910609
"application/json": {
1061010610
"schema": {
@@ -10620,7 +10620,8 @@
1062010620
"text/event-stream": {
1062110621
"schema": {
1062210622
"type": "string",
10623-
"format": "text/event-stream"
10623+
"format": "text/event-stream",
10624+
"description": "Server-Sent Events stream when the JSON-RPC method is message/stream"
1062410625
},
1062510626
"example": "data: {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{}}\n\n"
1062610627
}
@@ -10637,7 +10638,7 @@
1063710638
"operationId": "handle_a2a_jsonrpc_a2a_post",
1063810639
"responses": {
1063910640
"200": {
10640-
"description": "Successful response: buffered JSON-RPC or HTTP payload for non-streaming calls, or a Server-Sent Events stream when the JSON-RPC method is message/stream.",
10641+
"description": "Successful response",
1064110642
"content": {
1064210643
"application/json": {
1064310644
"schema": {
@@ -10653,7 +10654,8 @@
1065310654
"text/event-stream": {
1065410655
"schema": {
1065510656
"type": "string",
10656-
"format": "text/event-stream"
10657+
"format": "text/event-stream",
10658+
"description": "Server-Sent Events stream when the JSON-RPC method is message/stream"
1065710659
},
1065810660
"example": "data: {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{}}\n\n"
1065910661
}
@@ -20507,5 +20509,103 @@
2050720509
]
2050820510
}
2050920511
}
20510-
}
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+
]
2051120611
}

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

src/app/main.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
from collections.abc import AsyncIterator
55
from contextlib import asynccontextmanager
6+
from typing import Final
67

78
import sentry_sdk # pyright: ignore[reportMissingImports]
89
from fastapi import FastAPI, HTTPException
@@ -34,6 +35,34 @@
3435

3536
service_name = configuration.configuration.name
3637

38+
# Global OpenAPI tags so every operation tag is declared (Spectral: operation-tag-defined).
39+
_OPENAPI_TAGS: Final[list[dict[str, str]]] = [
40+
{"name": "a2a", "description": "Agent-to-Agent (A2A) protocol."},
41+
{"name": "authorized", "description": "Authorization probe."},
42+
{"name": "config", "description": "Service configuration."},
43+
{"name": "conversations_v1", "description": "Conversations API v1."},
44+
{"name": "conversations_v2", "description": "Conversations API v2."},
45+
{"name": "feedback", "description": "User feedback."},
46+
{"name": "health", "description": "Health and readiness probes."},
47+
{"name": "info", "description": "Service information."},
48+
{"name": "mcp-auth", "description": "MCP client authentication options."},
49+
{"name": "mcp-servers", "description": "MCP server registration."},
50+
{"name": "metrics", "description": "Prometheus metrics."},
51+
{"name": "models", "description": "LLM models."},
52+
{"name": "prompts", "description": "Prompt management."},
53+
{"name": "providers", "description": "Inference providers."},
54+
{"name": "query", "description": "Non-streaming query."},
55+
{"name": "rags", "description": "RAG configuration."},
56+
{"name": "responses", "description": "OpenAI-compatible Responses API."},
57+
{"name": "rlsapi-v1", "description": "RLS API v1 (inference)."},
58+
{"name": "root", "description": "Service root."},
59+
{"name": "shields", "description": "Safety shields."},
60+
{"name": "streaming_query", "description": "Streaming query (SSE)."},
61+
{"name": "streaming_query_interrupt", "description": "Streaming interrupt."},
62+
{"name": "tools", "description": "Tools."},
63+
{"name": "vector-stores", "description": "Vector stores and files."},
64+
]
65+
3766

3867
# running on FastAPI startup
3968
@asynccontextmanager
@@ -111,8 +140,9 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
111140
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
112141
},
113142
servers=[
114-
{"url": "http://localhost:8080/", "description": "Locally running service"}
143+
{"url": "http://localhost:8080", "description": "Locally running service"}
115144
],
145+
openapi_tags=_OPENAPI_TAGS,
116146
lifespan=lifespan,
117147
)
118148

0 commit comments

Comments
 (0)