fix: silence YAML-docstring noise from custom-app OpenAPI scan (#317)

This commit is contained in:
jfilipiuk
2026-07-13 16:38:28 +02:00
committed by GitHub
parent 88ac9f5ba1
commit 753c745405
3 changed files with 213 additions and 1 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ async def get_models(_request: Request) -> JSONResponse:
``discover_ollama_models()`` call, same 1.5-s timeout, same
fail-soft semantics (the probe returns ``[]`` on any error, never
raises). The TUI's "Custom Ollama model…" sentinel is intentionally
omitted: that's a widget-specific input affordance, not part of
omitted — that's a widget-specific input affordance, not part of
the registry surface.
``default`` reflects the deployment's currently-configured fallback
+60
View File
@@ -729,6 +729,66 @@ def _patch_openai_capture_reasoning_content() -> None:
_patch_openai_capture_reasoning_content()
# ---------------------------------------------------------------------------
# Patch (module-level): silence langgraph_api's OpenAPI schema-generation
# warnings for endpoints whose docstrings aren't valid YAML.
#
# Upstream ``langgraph_api.utils.SchemaGenerator.get_schema`` calls
# ``parse_docstring`` (inherited from Starlette's ``BaseSchemaGenerator``)
# on every registered endpoint. When the docstring is prose with stray
# ``:`` characters, ``yaml.safe_load`` raises and upstream logs the
# failure + full traceback at WARNING level. It then falls back to
# ``{"description": docstring}`` — the endpoint still ends up in the
# schema with its prose as the description, just without structured
# ``parameters``/``responses``/``tags`` fields.
#
# The fallback path is fine; the warning + traceback is just noise. And
# it's only triggered for our deploy because mounting any custom Starlette
# app (``EvoScientist/langgraph_dev/http.py``) makes upstream call
# ``update_openapi_spec`` at startup — which iterates EVERY route,
# including upstream's own endpoints whose prose docstrings predate the
# YAML convention.
#
# Fix: wrap ``parse_docstring`` itself and absorb ``yaml.YAMLError`` by
# returning the same fallback shape upstream's except branch produces.
# Non-YAML exceptions are deliberately left to propagate — upstream's
# ``get_schema`` already catches them and logs WARNING + traceback, so
# unexpected failures remain debuggable. Patching ``parse_docstring`` (a
# small, stable method) instead of ``get_schema`` (the larger loop body)
# minimizes our exposure to upstream churn.
# ---------------------------------------------------------------------------
_langgraph_schema_silenced_patched = False
def _patch_langgraph_schema_generator_silence_warnings() -> None:
global _langgraph_schema_silenced_patched
if _langgraph_schema_silenced_patched:
return
try:
import langgraph_api.utils as _lgapi_utils
import yaml
_SchemaGenerator = _lgapi_utils.SchemaGenerator
_orig_parse_docstring = _SchemaGenerator.parse_docstring
def _patched_parse_docstring(self: Any, func: Any) -> dict[str, Any]:
try:
return _orig_parse_docstring(self, func)
except yaml.YAMLError:
return {"description": getattr(func, "__doc__", None) or ""}
_SchemaGenerator.parse_docstring = _patched_parse_docstring
_langgraph_schema_silenced_patched = True
except Exception:
# Patches are loader-safe: never crash the import. Silent failure
# here just leaves the upstream warnings visible in deploy logs,
# which is a benign fallback.
pass
_patch_langgraph_schema_generator_silence_warnings()
# ---------------------------------------------------------------------------
# Patch (lazy, OpenRouter only): strip OpenAI-Responses encrypted reasoning
# items from outgoing assistant messages.
@@ -0,0 +1,152 @@
"""Regression tests for the langgraph_api SchemaGenerator silencing patch.
Reproducer: mounting our ``/api/models`` custom Starlette app makes
langgraph_api call ``update_openapi_spec`` at startup, which iterates
EVERY route (ours + upstream's). Endpoints whose docstrings aren't
valid YAML hit a warning + traceback in the deploy log — purely noise,
since the existing fallback path already produces a usable schema
entry. The patch keeps the fallback shape but silences the log spam.
"""
from __future__ import annotations
import os
# ``langgraph_api.config`` reads several required env vars at import
# time via starlette's ``Config(...)`` helper. We don't actually use the
# DB or Redis here — any non-empty string keeps the loader happy.
os.environ.setdefault("DATABASE_URI", "sqlite:///:memory:")
os.environ.setdefault("REDIS_URI", "redis://localhost:6379")
# Importing patches.py applies the eager module-level monkey-patch.
import langgraph_api.utils as _lgapi_utils
import EvoScientist.llm.patches as _patches
# Re-invoke the patch after env vars are set. Required because earlier test
# modules (e.g. test_llm.py) import patches.py *without* DATABASE_URI/
# REDIS_URI, which makes ``langgraph_api.utils`` fail to import inside the
# patch's bare ``except``; the loader swallows it and the flag stays False
# forever (Python won't re-run module-level code on subsequent imports).
# The patch function is idempotent (early-return on the flag), so calling
# it here is a no-op when the patch already landed and a successful retry
# when the prior import failed.
_patches._patch_langgraph_schema_generator_silence_warnings()
class _FakeEndpoint:
"""Minimal Starlette-like endpoint info for the schema generator."""
def __init__(self, path: str, method: str, func):
self.path = path
self.http_method = method
self.func = func
class _DocstringFixture:
"""The kinds of docstrings the patched generator must handle."""
@staticmethod
def prose_with_colon():
"""Endpoint summary.
Query params:
id: The thing you want.
"""
@staticmethod
def valid_yaml():
"""
summary: A valid YAML docstring.
description: Stays structured.
"""
@staticmethod
def no_docstring():
pass
def _generator():
return _lgapi_utils.SchemaGenerator(
{"openapi": "3.1.0", "info": {"title": "test", "version": "0"}}
)
def test_prose_docstring_no_longer_logs_warning():
"""The patched ``parse_docstring`` must silence upstream's structlog
WARNING when ``yaml.safe_load`` fails on a prose docstring.
Inverts the patch first to prove the fixture actually trips
``yaml.safe_load`` — without this baseline assertion the test would
pass vacuously if the fixture stopped triggering the failure path
(e.g. if upstream changed how docstrings are pre-processed).
"""
from structlog.testing import capture_logs
gen = _generator()
endpoint = _FakeEndpoint("/x", "get", _DocstringFixture.prose_with_colon)
gen.get_endpoints = lambda _routes: [endpoint]
patched_parse = _lgapi_utils.SchemaGenerator.parse_docstring
# Phase 1: baseline. Drop the subclass override so MRO falls through
# to Starlette's BaseSchemaGenerator.parse_docstring, which is what
# production hits before our patch installs.
del _lgapi_utils.SchemaGenerator.parse_docstring
try:
with capture_logs() as baseline_records:
gen.get_schema([])
finally:
_lgapi_utils.SchemaGenerator.parse_docstring = patched_parse
baseline_warnings = [r for r in baseline_records if r.get("log_level") == "warning"]
assert any(
"Unable to parse docstring" in r.get("event", "") for r in baseline_warnings
), "fixture no longer trips parse_docstring — test would pass vacuously"
# Phase 2: with the patch reinstated, the same call must emit no
# warning records.
with capture_logs() as patched_records:
schema = gen.get_schema([])
assert [r for r in patched_records if r.get("log_level") == "warning"] == []
# Schema still has the fallback shape — fixture's prose becomes the
# description verbatim (with leading/trailing whitespace from the
# docstring preserved by upstream's fallback path).
entry = schema["paths"]["/x"]["get"]
assert "description" in entry
assert "Query params" in entry["description"]
def test_valid_yaml_docstring_keeps_structured_parse():
"""Endpoints with parseable YAML keep their structured metadata —
we only changed the failure branch, not the success path.
"""
gen = _generator()
endpoint = _FakeEndpoint("/y", "get", _DocstringFixture.valid_yaml)
gen.get_endpoints = lambda _routes: [endpoint]
schema = gen.get_schema([])
entry = schema["paths"]["/y"]["get"]
assert entry.get("summary") == "A valid YAML docstring."
assert entry.get("description") == "Stays structured."
def test_no_docstring_still_handled():
"""Endpoints with ``__doc__ = None`` must not raise — fallback uses
empty string for ``description``.
"""
gen = _generator()
endpoint = _FakeEndpoint("/z", "get", _DocstringFixture.no_docstring)
gen.get_endpoints = lambda _routes: [endpoint]
schema = gen.get_schema([])
entry = schema["paths"]["/z"]["get"]
# Either description="" (fallback path) or structured (if YAML parse
# of None happens to succeed somehow — implementation detail).
# The contract is just "no exception, entry exists".
assert isinstance(entry, dict)
def test_patch_flag_set():
from EvoScientist.llm.patches import _langgraph_schema_silenced_patched
assert _langgraph_schema_silenced_patched is True