From 9d9ce63c82d56d57e6753fa2e0f92f7fa0cc1caa Mon Sep 17 00:00:00 2001 From: Drake Thomsen <120344051+ThomsenDrake@users.noreply.github.com> Date: Thu, 18 Jun 2026 09:15:01 -0400 Subject: [PATCH] feat: add llms.txt endpoint for AI agents (#3877) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Storm Knight Co-authored-by: ThomsenDrake Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com> Co-authored-by: dni ⚡ --- lnbits/app.py | 4 +++ lnbits/llms_txt.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 lnbits/llms_txt.py diff --git a/lnbits/app.py b/lnbits/app.py index a0c9abbef..46caa188c 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -40,6 +40,7 @@ from lnbits.core.tasks import ( ) from lnbits.exceptions import register_exception_handlers from lnbits.helpers import version_parse +from lnbits.llms_txt import create_llms_txt_route from lnbits.settings import settings from lnbits.tasks import ( cancel_all_tasks, @@ -102,6 +103,9 @@ async def startup(app: FastAPI): # register core routes init_core_routers(app) + # register llms.txt endpoint for AI agents + create_llms_txt_route(app) + # initialize tasks register_async_tasks() diff --git a/lnbits/llms_txt.py b/lnbits/llms_txt.py new file mode 100644 index 000000000..13816c4b5 --- /dev/null +++ b/lnbits/llms_txt.py @@ -0,0 +1,78 @@ +"""Generate llms.txt markdown from FastAPI OpenAPI schema for AI agents.""" + +from typing import Any + +from fastapi import FastAPI +from fastapi.responses import PlainTextResponse + + +def generate_llms_txt(app: FastAPI) -> str: + """Convert an OpenAPI schema to llms.txt markdown format.""" + openapi_schema = app.openapi() + lines: list[str] = [] + + # H1: API Title + info = openapi_schema.get("info", {}) + title = info.get("title", "API") + lines.append(f"# {title}") + lines.append("") + + # Blockquote: Description + description = info.get("description") + if description: + for line in description.strip().split("\n"): + lines.append(f"> {line}") + lines.append("") + + # Group endpoints by tag + paths = openapi_schema.get("paths", {}) + endpoints_by_tag: dict[str, list[dict[str, Any]]] = {} + + for path, path_item in paths.items(): + for method in ["get", "post", "put", "patch", "delete", "head", "options"]: + if method not in path_item: + continue + operation = path_item[method] + tags = operation.get("tags", ["Endpoints"]) + tag = tags[0] if tags else "Endpoints" + if tag not in endpoints_by_tag: + endpoints_by_tag[tag] = [] + endpoints_by_tag[tag].append( + { + "path": path, + "method": method.upper(), + "operation": operation, + } + ) + + # Generate sections by tag + for tag, endpoints in endpoints_by_tag.items(): + lines.append(f"## {tag}") + lines.append("") + for endpoint in endpoints: + method = endpoint["method"] + path = endpoint["path"] + operation = endpoint["operation"] + summary = operation.get("summary", "") + if summary: + lines.append(f"### `{method} {path}` - {summary}") + else: + lines.append(f"### `{method} {path}`") + lines.append("") + lines.append("") + + return "\n".join(lines).strip() + "\n" + + +def create_llms_txt_route(app: FastAPI) -> None: + """Add a /llms.txt endpoint to the app.""" + + @app.get( + "/llms.txt", + response_class=PlainTextResponse, + include_in_schema=False, + summary="Get LLM-friendly API documentation", + ) + async def get_llms_txt() -> str: + """Return the API documentation in llms.txt markdown format.""" + return generate_llms_txt(app)