fix(proxy): keep the upgrade notice off the request path and count area-labelled release notes (#51027)

* fix(proxy): keep the upgrade notice off the request path and count area-labelled release notes

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(proxy): regenerate dashboard API types and read the release cache through one typed helper

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): respond 502 from /get/latest_release_info when no release note entry is countable

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): respond 502 when the release fetch failed and announce a release with no countable notes without counts

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): re-ask for the latest release while the proxy is still fetching it and keep fetch diagnostics out of the 502 body

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): keep the known release on screen while the proxy refetches it

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): answer 202 while the release is being fetched so the dashboard polls only then

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): name the release query options to stay inside the lint budget

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): return the 202 as a response value instead of mutating the injected response

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): fetch the release on the background client so its 502 never reaches the error handler

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Kerry Lu 2026-11-10 18:43:06 -08:00 • committed by GitHub
parent 917fd11966
commit bb89296adf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 323 additions and 47 deletions

View file

@ -9,7 +9,8 @@ from types import MappingProxyType
from typing import Annotated, Final, Literal, Protocol, TypeAlias
import httpx
from fastapi import APIRouter, Depends
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import ValidationError
from litellm._logging import verbose_proxy_logger
@ -29,11 +30,13 @@ LATEST_RELEASE_UNAVAILABLE_CACHE_TTL_SECONDS: Final = 5 * 60
LATEST_RELEASE_CACHE_KEY: Final = "latest_release_info"
_RELEASE_BULLET_PATTERN: Final = re.compile(r"^\*\s+(?:([A-Za-z]+)(?:\([^)]*\))?!?:\s)?\S")
_AREA_BULLET_PATTERN: Final = re.compile(r"^-\s+\*\*[^*]+\*\*\s+([A-Za-z]+)")
_NEW_CONTRIBUTOR_PATTERN: Final = re.compile(r"^\*\s+@\S+ made their first contribution\b")
_CHANGELOG_PAGE_PATTERN: Final = re.compile(rf"{re.escape(CHANGELOG_URL)}v(\d+)\.(\d+)\.(\d+)/")
_Bucket: TypeAlias = Literal["new_features", "bug_fixes", "other_updates"]
_PREFIX_BUCKETS: Final[Mapping[str, _Bucket]] = MappingProxyType({"feat": "new_features", "fix": "bug_fixes"})
_VERB_BUCKETS: Final[Mapping[str, _Bucket]] = MappingProxyType({"added": "new_features", "fixed": "bug_fixes"})
class LatestReleaseInfo(LiteLLMBaseModel):
@ -97,6 +100,9 @@ def _default_release_check_enabled() -> bool:
def _bucket_for(line: str) -> _Bucket | None:
if _NEW_CONTRIBUTOR_PATTERN.match(line) is not None:
return None
area_match: Final = _AREA_BULLET_PATTERN.match(line)
if area_match is not None:
return _VERB_BUCKETS.get(area_match.group(1).lower(), "other_updates")
match: Final = _RELEASE_BULLET_PATTERN.match(line)
if match is None:
return None
@ -105,7 +111,8 @@ def _bucket_for(line: str) -> _Bucket | None:
def count_release_bullets(body: str) -> Mapping[_Bucket, int]:
"""Bucket release-note bullets by conventional-commit type or ``other_updates``."""
"""Bucket release-note bullets by conventional-commit type (``* fix(x): ...``) or by the verb after the area label
(``- **Area:** Fixed ...``), falling back to ``other_updates``."""
return MappingProxyType(Counter(bucket for line in body.splitlines() if (bucket := _bucket_for(line)) is not None))
@ -200,15 +207,20 @@ async def fetch_latest_release(client: _AsyncGetClient) -> LatestReleaseInfo | L
)
def _cached_release(cache: InMemoryCache) -> LatestReleaseInfo | LatestReleaseUnavailable | None:
cached: Final = cache.get_cache(LATEST_RELEASE_CACHE_KEY)
return cached if isinstance(cached, (LatestReleaseInfo, LatestReleaseUnavailable)) else None
async def get_latest_release_info(
client: _AsyncGetClient, cache: InMemoryCache, fetch_lock: asyncio.Lock
) -> LatestReleaseInfo | LatestReleaseUnavailable:
cached: Final = cache.get_cache(LATEST_RELEASE_CACHE_KEY)
if isinstance(cached, (LatestReleaseInfo, LatestReleaseUnavailable)):
cached: Final = _cached_release(cache)
if cached is not None:
return cached
async with fetch_lock:
cached_after_lock: Final = cache.get_cache(LATEST_RELEASE_CACHE_KEY)
if isinstance(cached_after_lock, (LatestReleaseInfo, LatestReleaseUnavailable)):
cached_after_lock: Final = _cached_release(cache)
if cached_after_lock is not None:
return cached_after_lock
result: Final = await fetch_latest_release(client)
ttl: Final = (
@ -220,28 +232,46 @@ async def get_latest_release_info(
return result
async def refresh_latest_release_info(client: _AsyncGetClient, cache: InMemoryCache, fetch_lock: asyncio.Lock) -> None:
result: Final = await get_latest_release_info(client=client, cache=cache, fetch_lock=fetch_lock)
if isinstance(result, LatestReleaseUnavailable):
verbose_proxy_logger.warning("LiteLLM: latest release info unavailable: %s", result.reason)
@router.get(
"/get/latest_release_info",
tags=["UI Settings"],
dependencies=[Depends(user_api_key_auth)],
response_model=LatestReleaseInfo | None,
responses={
202: {"description": "The release is being fetched in the background, so ask again shortly", "model": None},
502: {"description": "The changelog or GitHub could not be reached or answered with something unusable"},
},
)
async def latest_release_info(
background_tasks: BackgroundTasks,
client: Annotated[_AsyncGetClient, Depends(_default_client)],
cache: Annotated[InMemoryCache, Depends(_default_cache)],
fetch_lock: Annotated[asyncio.Lock, Depends(_default_fetch_lock)],
release_check_enabled: Annotated[bool, Depends(_default_release_check_enabled)],
) -> LatestReleaseInfo | None:
) -> LatestReleaseInfo | JSONResponse | None:
"""
Newest stable release on the LiteLLM changelog, linked to its changelog page, with the PR count of its whole
minor line (e.g. v1.105.0 + v1.105.1) from GitHub split into new features, bug fixes and other updates.
Returns null when the changelog or GitHub can't be reached so the dashboard upgrade banner simply doesn't render.
Never waits on the changelog or GitHub. It answers from the cache and, when the cache is empty or expired, responds
202 with null and fetches after the response is sent, so a later call returns the release. Responds 502 once that
fetch has failed because the changelog or GitHub couldn't be reached or answered with something unusable, so the dashboard
upgrade notice simply doesn't render. The three counts are all zero when the release notes list no change in a
format this proxy can count.
Set LITELLM_DISABLE_LATEST_RELEASE_CHECK=true to return null without making any outbound request.
"""
if not release_check_enabled:
return None
result: Final = await get_latest_release_info(client=client, cache=cache, fetch_lock=fetch_lock)
if isinstance(result, LatestReleaseUnavailable):
verbose_proxy_logger.warning("LiteLLM: latest release info unavailable: %s", result.reason)
return None
return result
cached: Final = _cached_release(cache)
if isinstance(cached, LatestReleaseInfo):
return cached
if isinstance(cached, LatestReleaseUnavailable):
raise HTTPException(status_code=502, detail="Latest release info unavailable")
if not fetch_lock.locked():
background_tasks.add_task(refresh_latest_release_info, client, cache, fetch_lock)
return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=None)

View file

@ -121,6 +121,53 @@ def _override_dependencies(client: _ByUrl, cache: InMemoryCache, role: LitellmUs
app.dependency_overrides[_default_fetch_lock] = lambda: asyncio.Lock()
class _AsgiGet:
"""One GET sent straight to the ASGI app, so the response body is readable while the app call is still running."""
def __init__(self, path: str) -> None:
self._chunks: list[bytes] = []
self._sent = asyncio.Event()
self.task = asyncio.create_task(
app(
{
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": "GET",
"scheme": "http",
"path": path,
"raw_path": path.encode(),
"query_string": b"",
"headers": [(b"host", b"proxy")],
"client": ("127.0.0.1", 1),
"server": ("proxy", 80),
},
self._receive,
self._send,
)
)
async def _receive(self) -> dict[str, object]:
return {"type": "http.request", "body": b"", "more_body": False}
async def _send(self, message: dict[str, object]) -> None:
if message["type"] != "http.response.body":
return
body = message.get("body", b"")
assert isinstance(body, bytes)
self._chunks.append(body)
if not message.get("more_body", False):
self._sent.set()
@property
def finished(self) -> bool:
return self.task.done()
async def body(self) -> bytes:
await self._sent.wait()
return b"".join(self._chunks)
@pytest.fixture
def http_client():
yield TestClient(app)
@ -152,6 +199,18 @@ class TestCountReleaseBullets:
== 0
)
def test_buckets_area_labelled_bullets_by_their_leading_verb(self):
counts = count_release_bullets(
"### Changes\n\n"
"- **OpenAI:** Fixed empty `api_key` sending the admin key (#1)\n"
"- **AWS Bedrock:** fixed Converse `service_tier` (#2)\n"
"- **MCP:** Added per-server timeouts (#3)\n"
"- **Routing and fallbacks:** Changed the default cooldown (#4)\n"
"- **UI:** Moved the upgrade notice into the bell (#5)\n"
"- plain dash line that is not a change entry\n"
)
assert (counts["new_features"], counts["bug_fixes"], counts["other_updates"]) == (1, 2, 2)
def test_empty_body_yields_zero_counts(self):
counts = count_release_bullets("")
assert (counts["new_features"], counts["bug_fixes"], counts["other_updates"]) == (0, 0, 0)
@ -387,19 +446,62 @@ class TestPatchReleaseAggregatesMinorLine:
class TestLatestReleaseInfoEndpoint:
def test_returns_release_stats_for_authenticated_user(self, http_client):
def test_first_request_returns_null_and_the_next_returns_the_fetched_release(self, http_client):
_override_dependencies(_minor_release(), _fresh_cache(), LitellmUserRoles.INTERNAL_USER)
response = http_client.get("/get/latest_release_info")
assert response.status_code == 200
assert response.json() == EXPECTED_INFO
first = http_client.get("/get/latest_release_info")
assert (first.status_code, first.json()) == (202, None)
second = http_client.get("/get/latest_release_info")
assert (second.status_code, second.json()) == (200, EXPECTED_INFO)
def test_returns_null_when_the_changelog_is_unreachable(self, http_client):
@pytest.mark.asyncio
async def test_responds_while_the_changelog_fetch_is_still_hanging(self):
release = asyncio.Event()
class _HangingClient(_ByUrl):
async def get(self, url: str, *, timeout: float | None = None) -> httpx.Response:
self.calls.append((url, timeout))
await release.wait()
raise httpx.ConnectTimeout("blackholed")
client: Final = _HangingClient({})
_override_dependencies(client, _fresh_cache(), LitellmUserRoles.PROXY_ADMIN)
lock: Final = asyncio.Lock()
app.dependency_overrides[_default_fetch_lock] = lambda: lock
try:
requests: Final = [_AsgiGet("/get/latest_release_info") for _ in range(3)]
bodies: Final = [await asyncio.wait_for(request.body(), timeout=5) for request in requests]
still_fetching: Final = [request for request in requests if not request.finished]
release.set()
await asyncio.gather(*(request.task for request in requests))
finally:
release.set()
for dependency in (user_api_key_auth, _default_client, _default_cache, _default_fetch_lock):
app.dependency_overrides.pop(dependency, None)
assert bodies == [b"null", b"null", b"null"]
assert len(still_fetching) == 1, "exactly one request should still be running the hanging fetch"
assert client.urls == [CHANGELOG_FEED_URL]
def test_responds_502_once_the_changelog_fetch_has_failed(self, http_client):
client: Final = _ByUrl({CHANGELOG_FEED_URL: httpx.ConnectError("boom")})
_override_dependencies(client, _fresh_cache(), LitellmUserRoles.PROXY_ADMIN)
first: Final = http_client.get("/get/latest_release_info")
assert (first.status_code, first.json()) == (202, None)
later: Final = [http_client.get("/get/latest_release_info") for _ in range(2)]
assert [response.status_code for response in later] == [502, 502]
assert later[0].json() == {"detail": "Latest release info unavailable"}
assert len(client.calls) == 1
def test_release_with_no_countable_notes_is_still_announced_with_zero_counts(self, http_client):
uncountable: Final = _github_response(payload=_release("v1.102.0", "## Changes\n\n1. Reworked the router\n"))
_override_dependencies(
_ByUrl({CHANGELOG_FEED_URL: httpx.ConnectError("boom")}), _fresh_cache(), LitellmUserRoles.PROXY_ADMIN
_minor_release(**{_tag_url("v1.102.0"): uncountable}), _fresh_cache(), LitellmUserRoles.PROXY_ADMIN
)
http_client.get("/get/latest_release_info")
response: Final = http_client.get("/get/latest_release_info")
assert (response.status_code, response.json()) == (
200,
{**EXPECTED_INFO, "new_features": 0, "bug_fixes": 0, "other_updates": 0},
)
response = http_client.get("/get/latest_release_info")
assert response.status_code == 200
assert response.json() is None
@pytest.mark.parametrize("name", ["LITELLM_DISABLE_LATEST_RELEASE_CHECK", "LITELLM_OFFLINE_MODE"])
@pytest.mark.parametrize(
@ -416,6 +518,8 @@ class TestLatestReleaseInfoEndpoint:
monkeypatch.setenv(name, flag)
client = _minor_release()
_override_dependencies(client, _fresh_cache(), LitellmUserRoles.PROXY_ADMIN)
first = http_client.get("/get/latest_release_info")
assert first.status_code == (200 if expected_body is None else 202)
response = http_client.get("/get/latest_release_info")
assert response.status_code == 200
assert response.json() == expected_body
@ -424,8 +528,8 @@ class TestLatestReleaseInfoEndpoint:
def test_repeated_requests_reuse_cache(self, http_client):
client = _minor_release()
_override_dependencies(client, _fresh_cache(), LitellmUserRoles.PROXY_ADMIN)
assert http_client.get("/get/latest_release_info").json() == EXPECTED_INFO
assert http_client.get("/get/latest_release_info").json() == EXPECTED_INFO
bodies = [http_client.get("/get/latest_release_info").json() for _ in range(3)]
assert bodies == [None, EXPECTED_INFO, EXPECTED_INFO]
assert len(client.calls) == 2
def test_rejects_unauthenticated_requests(self, monkeypatch):

View file

@ -0,0 +1,76 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { registerErrorHandler } from "@/lib/http/runtime";
import {
PENDING_RELEASE_POLL_MS,
fetchLatestRelease,
keepKnownRelease,
pendingReleasePollMs,
} from "./useLatestReleaseInfo";
const RELEASE = { version: "1.103.0", new_features: 1, bug_fixes: 2, other_updates: 3, release_url: "https://x" };
describe("pendingReleasePollMs", () => {
it("asks again soon while the proxy is still fetching the release", () => {
const state = { status: "success", data: { release: null, pending: true } };
expect(pendingReleasePollMs(state)).toBe(PENDING_RELEASE_POLL_MS);
});
it("keeps asking while a known release is shown and the proxy is refetching it", () => {
const state = { status: "success", data: { release: RELEASE, pending: true } };
expect(pendingReleasePollMs(state)).toBe(PENDING_RELEASE_POLL_MS);
});
it("stops once the release arrives", () => {
expect(pendingReleasePollMs({ status: "success", data: { release: RELEASE, pending: false } })).toBe(false);
});
it("stops once the proxy reports the fetch failed", () => {
expect(pendingReleasePollMs({ status: "error", data: { release: null, pending: true } })).toBe(false);
});
it("does not ask again when the release check is turned off", () => {
expect(pendingReleasePollMs({ status: "success", data: { release: null, pending: false } })).toBe(false);
});
});
describe("keepKnownRelease", () => {
it("keeps showing the known release while the proxy refetches it", () => {
const kept = keepKnownRelease({ release: RELEASE, pending: false }, { release: null, pending: true });
expect(kept).toEqual({ release: RELEASE, pending: true });
});
it("takes a newer release over the known one", () => {
const newer = { release: { ...RELEASE, version: "1.104.0" }, pending: false };
expect(keepKnownRelease({ release: RELEASE, pending: true }, newer)).toBe(newer);
});
it("drops the known release when the release check has been turned off", () => {
const off = { release: null, pending: false };
expect(keepKnownRelease({ release: RELEASE, pending: false }, off)).toBe(off);
});
it("records a first answer as is", () => {
const first = { release: null, pending: true };
expect(keepKnownRelease(undefined, first)).toBe(first);
});
});
describe("fetchLatestRelease", () => {
afterEach(() => {
vi.unstubAllGlobals();
registerErrorHandler(() => {});
});
it("keeps a failed release fetch away from the dashboard error handler", async () => {
const onError = vi.fn();
registerErrorHandler(onError);
const unavailable = new Response(JSON.stringify({ detail: "Latest release info unavailable" }), {
status: 502,
headers: { "content-type": "application/json" },
});
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(unavailable));
await expect(fetchLatestRelease()).rejects.toMatchObject({ status: 502 });
expect(onError).not.toHaveBeenCalled();
});
});

View file

@ -1,16 +1,41 @@
import { $api } from "@/lib/http/api";
import { useQuery } from "@tanstack/react-query";
import { backgroundFetchClient } from "@/lib/http/api";
import type { components } from "@/lib/http/schema";
import { createQueryKeys } from "../common/queryKeysFactory";
export type LatestReleaseInfo = components["schemas"]["LatestReleaseInfo"];
export const useLatestReleaseInfo = (accessToken: string | null | undefined) =>
$api.useQuery(
"get",
"/get/latest_release_info",
{},
{
enabled: Boolean(accessToken),
staleTime: 60 * 60 * 1000,
retry: false,
},
);
export type LatestReleaseAnswer = { release: LatestReleaseInfo | null; pending: boolean };
export const PENDING_RELEASE_POLL_MS = 5 * 1000;
export const latestReleaseKeys = createQueryKeys("latestRelease");
export const pendingReleasePollMs = (state: { status: string; data?: LatestReleaseAnswer }) =>
state.status === "success" && state.data?.pending ? PENDING_RELEASE_POLL_MS : false;
export const keepKnownRelease = (known: unknown, answer: unknown) => {
const previous = known as LatestReleaseAnswer | undefined;
const next = answer as LatestReleaseAnswer;
return next.pending && previous?.release ? { ...next, release: previous.release } : next;
};
export const fetchLatestRelease = async (): Promise<LatestReleaseAnswer> => {
const { data, response } = await backgroundFetchClient.GET("/get/latest_release_info");
return { release: data ?? null, pending: response.status === 202 };
};
export const useLatestReleaseInfo = (accessToken: string | null | undefined) => {
const options = {
queryKey: latestReleaseKeys.all,
queryFn: fetchLatestRelease,
enabled: Boolean(accessToken),
staleTime: 60 * 60 * 1000,
retry: false,
refetchInterval: (query: { state: { status: string; data?: LatestReleaseAnswer } }) =>
pendingReleasePollMs(query.state),
structuralSharing: keepKnownRelease,
};
const { data, isError } = useQuery(options);
return { data: isError ? undefined : data?.release };
};

View file

@ -75,6 +75,21 @@ describe("NotificationsBellView", () => {
expect(announcement("LiteLLM v1.103.0 is available")).toHaveTextContent("1 new feature, 1 fix, and 1 other update");
});
it("leaves the counts out when the release notes yielded none", async () => {
const user = userEvent.setup();
renderWithProviders(
<NotificationsBellView
currentVersion="1.102.0"
latestRelease={{ ...RELEASE, new_features: 0, bug_fixes: 0, other_updates: 0 }}
/>,
);
await user.click(bell(2));
const upgrade = announcement("LiteLLM v1.103.0 is available");
expect(upgrade).toHaveTextContent("Your current version is v1.102.0");
expect(upgrade).not.toHaveTextContent("0 new features");
expect(upgrade).not.toHaveTextContent("other update");
});
it.each([
["the running version is unknown", undefined, RELEASE],
["the latest release is unavailable", "1.102.0", null],

View file

@ -45,12 +45,22 @@ const autoRouterAnnouncement = (): Announcement => ({
href: AUTO_ROUTER_DOCS_URL,
});
const describeRelease = ({ new_features, bug_fixes, other_updates }: LatestReleaseInfo): string =>
[
i18n.t("shell.notifications.release.newFeatures", { count: new_features }),
i18n.t("shell.notifications.release.fixes", { count: bug_fixes }),
i18n.t("shell.notifications.release.otherUpdates", { count: other_updates }),
].join(", ");
const describeUpgrade = (
currentVersion: string,
{ new_features, bug_fixes, other_updates }: LatestReleaseInfo,
): string => {
if (new_features + bug_fixes + other_updates === 0) {
return i18n.t("shell.notifications.upgrade.descriptionWithoutCounts", { current: currentVersion });
}
return i18n.t("shell.notifications.upgrade.description", {
summary: [
i18n.t("shell.notifications.release.newFeatures", { count: new_features }),
i18n.t("shell.notifications.release.fixes", { count: bug_fixes }),
i18n.t("shell.notifications.release.otherUpdates", { count: other_updates }),
].join(", "),
current: currentVersion,
});
};
const upgradeAnnouncement = (
currentVersion: string | null | undefined,
@ -62,10 +72,7 @@ const upgradeAnnouncement = (
return {
readKey: `litellm:upgradeBannerDismissed:${latestRelease.version}`,
title: i18n.t("shell.notifications.upgrade.title", { version: latestRelease.version }),
description: i18n.t("shell.notifications.upgrade.description", {
summary: describeRelease(latestRelease),
current: currentVersion,
}),
description: describeUpgrade(currentVersion, latestRelease),
linkLabel: i18n.t("shell.notifications.upgrade.link"),
href: latestRelease.release_url,
};

View file

@ -8208,6 +8208,7 @@
"upgrade": {
"title": "LiteLLM v{{version}} is available",
"description": "{{summary}}. Your current version is v{{current}}",
"descriptionWithoutCounts": "Your current version is v{{current}}",
"link": "View release notes"
},
"announcement": {

View file

@ -6077,7 +6077,11 @@ export interface paths {
* Latest Release Info
* @description Newest stable release on the LiteLLM changelog, linked to its changelog page, with the PR count of its whole
* minor line (e.g. v1.105.0 + v1.105.1) from GitHub split into new features, bug fixes and other updates.
* Returns null when the changelog or GitHub can't be reached so the dashboard upgrade banner simply doesn't render.
* Never waits on the changelog or GitHub. It answers from the cache and, when the cache is empty or expired, responds
* 202 with null and fetches after the response is sent, so a later call returns the release. Responds 502 once that
* fetch has failed because the changelog or GitHub couldn't be reached or answered with something unusable, so the dashboard
* upgrade notice simply doesn't render. The three counts are all zero when the release notes list no change in a
* format this proxy can count.
* Set LITELLM_DISABLE_LATEST_RELEASE_CHECK=true to return null without making any outbound request.
*/
get: operations["latest_release_info_get_latest_release_info_get"];
@ -63074,6 +63078,20 @@ export interface operations {
"application/json": components["schemas"]["LatestReleaseInfo"] | null;
};
};
/** @description The release is being fetched in the background, so ask again shortly */
202: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description The changelog or GitHub could not be reached or answered with something unusable */
502: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
get_mcp_semantic_filter_settings_get_mcp_semantic_filter_settings_get: {