mirror of
https://github.com/langchain-ai/docs.git
synced 2026-07-19 16:33:35 -04:00
docs(langsmith): group v2 endpoints by resource with standardized titles (#4935)
## Why The LangSmith Platform OpenAPI spec mixes v1 and v2 endpoints in one file, and the backend already tags v2 operations by resource (`runs`, `datasets`, `threads`). This reworks how the reference presents them so a v2 endpoint sits with its v1 sibling and is easy to spot, and cleans up inconsistent endpoint titles across the whole reference. All changes are driven by `scripts/process_langsmith_openapi.py` (which runs on every spec sync), so they re-derive automatically and need no manual upkeep. ## What changed - **Group v2 by resource.** Stop collecting v2 endpoints in a separate bucket; let their backend resource tags place them alongside v1 (e.g. runs → Tracing), each marked `(v2)`. - **Match v1/v2 wording.** Paired endpoints now read identically via canonical-title overrides: "Query runs" / "Query runs (v2)", "Read run" / "Read run (v2)", "Share run" / "Share run (v2)". - **Sentence-case every title,** preserving an acronym/proper-noun allowlist (`API`, `AWS`, `SCIM`, `TTL`, `OAuth2`, `URL`, `WebSocket`, …). - **Normalize markers:** `[Beta] X` becomes `X (Beta)`, and trailing `X V2` becomes `X (v2)`. - **Threads** moved into its own group. ## Relationship to `main` This **supersedes the earlier "v2 endpoints" bucket approach** already on main — an intentional flip to resource-based grouping after reviewing how v2 actually spreads across resources. ## Please review carefully - **`V2_TITLE_OVERRIDES`** in the script — the three canonical titles are explicit, because the v1/v2 equivalence is semantic and even crosses HTTP methods (POST "Share a run" ≈ PUT "Share Run"), so it cannot be auto-inferred. New matching pairs get added here. - **`TITLE_PRESERVE`** acronym allowlist — anything missing gets lowercased; anything wrong gets over-preserved. - The large `langsmith-platform-openapi.json` diff is fully generated output; the script is the source of truth. ## Known limitation Within a resource group the v2 endpoints cluster together rather than each sitting directly beside its v1 twin. Mintlify sorts endpoints within a group by path, which cannot be overridden through the spec. Literal adjacency would require script-generated navigation, deferred by decision. ## AI involvement Authored with Claude Code (investigation, script changes, and spec regeneration); reviewed by @lnhsingh. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
5faa835858
commit
e64690f7ff
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import sys
|
||||
import urllib.error
|
||||
@@ -89,24 +90,37 @@ HIDDEN_PATH_PREFIXES: list[str] = [
|
||||
"/v2/sandboxes/internal/",
|
||||
]
|
||||
|
||||
# Newer v2 API endpoints (paths under ``/v2/``) are pulled into a dedicated
|
||||
# sidebar group so readers notice them, rather than being scattered through the
|
||||
# feature groups alongside their v1 counterparts.
|
||||
# v2 API endpoints (paths under ``/v2/``) carry their backend resource tag
|
||||
# (runs, datasets, threads), so they group with their v1 siblings automatically.
|
||||
# Append a "(v2)" marker to their titles so the two versions are distinguishable
|
||||
# in the sidebar. Sandboxes are a v2-only feature with no v1 counterpart, so they
|
||||
# are excluded from the marker.
|
||||
V2_PATH_PREFIX = "/v2/"
|
||||
V2_TAG = "v2"
|
||||
V2_GROUP = "v2 endpoints"
|
||||
|
||||
# v2 paths that are standalone features (not a newer version of a v1 endpoint)
|
||||
# stay in their own feature group instead of the v2 group.
|
||||
V2_GROUP_EXCLUDE_PREFIXES: list[str] = [
|
||||
V2_LABEL_SUFFIX = " (v2)"
|
||||
V2_LABEL_EXCLUDE_PREFIXES: list[str] = [
|
||||
"/v2/sandboxes/",
|
||||
]
|
||||
|
||||
# Canonical base titles for v2 operations whose backend wording diverges from
|
||||
# their v1 sibling, so the two versions read identically (the "(v2)" suffix is
|
||||
# added automatically). Keyed by (HTTP method, path).
|
||||
V2_TITLE_OVERRIDES: dict[tuple[str, str], str] = {
|
||||
("POST", "/v2/runs/query"): "Query runs",
|
||||
("GET", "/v2/runs/{run_id}"): "Read run",
|
||||
("POST", "/v2/runs/{run_id}/share"): "Share run",
|
||||
}
|
||||
|
||||
# Acronyms and proper nouns to preserve verbatim when sentence-casing titles.
|
||||
TITLE_PRESERVE: set[str] = {
|
||||
"API", "AWS", "GitHub", "HTTP", "HTTPS", "ID", "JSON", "LangGraph",
|
||||
"LangSmith", "LCU", "LLM", "MCP", "NPS", "OAuth2", "SCIM", "SDK", "SSO",
|
||||
"TCP", "TTL", "UI", "URI", "URL", "WebSocket",
|
||||
}
|
||||
_TITLE_PRESERVE_BY_UPPER: dict[str, str] = {t.upper(): t for t in TITLE_PRESERVE}
|
||||
|
||||
# Map raw tag names to human-readable group headings (``x-group``).
|
||||
# Tags not listed here keep their original name as the group heading.
|
||||
TAG_GROUPS: dict[str, str] = {
|
||||
# Newer v2 endpoints (see V2_GROUP).
|
||||
V2_TAG: V2_GROUP,
|
||||
# Tracing
|
||||
"run": "Tracing",
|
||||
"runs": "Tracing",
|
||||
@@ -185,14 +199,15 @@ TAG_GROUPS: dict[str, str] = {
|
||||
"public": "System",
|
||||
"ace": "System",
|
||||
"backfills": "System",
|
||||
"threads": "System",
|
||||
# Threads
|
||||
"threads": "Threads",
|
||||
}
|
||||
|
||||
# Display order for groups in the generated docs sidebar.
|
||||
# Groups not listed here are appended alphabetically after the listed ones.
|
||||
GROUP_ORDER: list[str] = [
|
||||
V2_GROUP,
|
||||
"Tracing",
|
||||
"Threads",
|
||||
"Datasets",
|
||||
"Evaluation",
|
||||
"Feedback & Annotation",
|
||||
@@ -227,6 +242,68 @@ def _should_hide_by_tags(tags: list[str]) -> bool:
|
||||
return any(tag in HIDDEN_TAGS for tag in tags)
|
||||
|
||||
|
||||
_BETA_PREFIX_RE = re.compile(r"^\[Beta\]\s*(.+)$")
|
||||
_TRAILING_V2_RE = re.compile(r"^(.*\S)\s+V2$")
|
||||
_TRAILING_MARKER_RE = re.compile(r"\s*\((Beta|v2)\)\s*$", re.IGNORECASE)
|
||||
|
||||
|
||||
def _sentence_case(text: str) -> str:
|
||||
"""Sentence-case *text*, preserving allow-listed acronyms and proper nouns."""
|
||||
words = text.split()
|
||||
out = []
|
||||
for i, word in enumerate(words):
|
||||
canonical = _TITLE_PRESERVE_BY_UPPER.get(word.upper())
|
||||
if canonical:
|
||||
out.append(canonical)
|
||||
elif i == 0:
|
||||
out.append(word[:1].upper() + word[1:].lower())
|
||||
else:
|
||||
out.append(word.lower())
|
||||
return " ".join(out)
|
||||
|
||||
|
||||
def _standardize_title(
|
||||
summary: str, *, override: str | None = None, v2_path: bool = False
|
||||
) -> str:
|
||||
"""Return a sentence-cased title with a trailing ``(Beta)``/``(v2)`` marker.
|
||||
|
||||
Strips a ``"[Beta] "`` prefix and a trailing ``" V2"`` and re-adds them as
|
||||
consistent suffixes. ``override`` replaces the base title (used to make a v2
|
||||
endpoint read identically to its v1 sibling); ``v2_path`` forces the
|
||||
``(v2)`` suffix for operations under ``/v2/``.
|
||||
"""
|
||||
text = summary.strip()
|
||||
beta = False
|
||||
v2 = v2_path
|
||||
# Strip any markers applied by a previous run so re-processing is idempotent.
|
||||
while True:
|
||||
match = _TRAILING_MARKER_RE.search(text)
|
||||
if not match:
|
||||
break
|
||||
if match.group(1).lower() == "beta":
|
||||
beta = True
|
||||
else:
|
||||
v2 = True
|
||||
text = text[: match.start()].strip()
|
||||
# Strip the raw backend markers ("[Beta] " prefix, trailing " V2").
|
||||
prefix = _BETA_PREFIX_RE.match(text)
|
||||
if prefix:
|
||||
beta = True
|
||||
text = prefix.group(1).strip()
|
||||
trailing_v2 = _TRAILING_V2_RE.match(text)
|
||||
if trailing_v2:
|
||||
v2 = True
|
||||
text = trailing_v2.group(1).strip()
|
||||
if override:
|
||||
text = override
|
||||
text = _sentence_case(text)
|
||||
if beta:
|
||||
text += " (Beta)"
|
||||
if v2:
|
||||
text += V2_LABEL_SUFFIX
|
||||
return text
|
||||
|
||||
|
||||
def process_spec(spec: dict) -> dict:
|
||||
"""Add ``x-hidden`` and ``x-group`` annotations to *spec* in place."""
|
||||
hidden_count = 0
|
||||
@@ -246,39 +323,28 @@ def process_spec(spec: dict) -> dict:
|
||||
operation["x-hidden"] = True
|
||||
hidden_count += 1
|
||||
|
||||
# 1b. Reassign visible v2 operations to a dedicated group so the newer
|
||||
# endpoints stand out. Runs after hiding so hidden v2 ops stay hidden.
|
||||
# 1b. Standardize endpoint titles: sentence-case them, normalize inline
|
||||
# markers to a trailing "(Beta)"/"(v2)" suffix, and force the "(v2)" suffix
|
||||
# on visible non-sandbox /v2/ operations so they are distinguishable from
|
||||
# their v1 siblings. Idempotent on re-runs.
|
||||
v2_count = 0
|
||||
for path, methods in spec.get("paths", {}).items():
|
||||
if not path.startswith(V2_PATH_PREFIX):
|
||||
continue
|
||||
if any(path.startswith(p) for p in V2_GROUP_EXCLUDE_PREFIXES):
|
||||
continue
|
||||
is_v2 = path.startswith(V2_PATH_PREFIX) and not any(
|
||||
path.startswith(p) for p in V2_LABEL_EXCLUDE_PREFIXES
|
||||
)
|
||||
for method, operation in methods.items():
|
||||
if not isinstance(operation, dict):
|
||||
continue
|
||||
if method in ("parameters", "summary", "description", "servers"):
|
||||
continue
|
||||
if operation.get("x-hidden"):
|
||||
continue
|
||||
operation["tags"] = [V2_TAG]
|
||||
v2_count += 1
|
||||
|
||||
# 1c. Move v2-tagged paths to the front of ``paths``. Mintlify orders
|
||||
# auto-generated sidebar groups by the order operations appear in the spec
|
||||
# (not by the ``tags`` array), so the v2 group must lead the paths to render
|
||||
# first, directly under the reference overview page.
|
||||
def _is_v2_path(methods: dict) -> bool:
|
||||
return any(
|
||||
isinstance(op, dict) and op.get("tags") == [V2_TAG]
|
||||
for op in methods.values()
|
||||
)
|
||||
|
||||
paths = spec.get("paths", {})
|
||||
v2_paths = {p: m for p, m in paths.items() if _is_v2_path(m)}
|
||||
if v2_paths:
|
||||
rest = {p: m for p, m in paths.items() if p not in v2_paths}
|
||||
spec["paths"] = {**v2_paths, **rest}
|
||||
v2_path = is_v2 and not operation.get("x-hidden")
|
||||
raw = operation.get("summary") or f"{method.upper()} {path}"
|
||||
override = V2_TITLE_OVERRIDES.get((method.upper(), path))
|
||||
operation["summary"] = _standardize_title(
|
||||
raw, override=override, v2_path=v2_path
|
||||
)
|
||||
if v2_path:
|
||||
v2_count += 1
|
||||
|
||||
# 2. Ensure top-level tags array exists and add x-group.
|
||||
if "tags" not in spec:
|
||||
@@ -323,7 +389,7 @@ def process_spec(spec: dict) -> dict:
|
||||
print(
|
||||
f"Processed {total_count} operations: "
|
||||
f"{hidden_count} hidden, {total_count - hidden_count} public "
|
||||
f"({v2_count} grouped under {V2_GROUP!r})",
|
||||
f"({v2_count} labeled '(v2)')",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user