mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-10 08:28:25 +00:00
Merge 0cbba87379 into be649f66a6
This commit is contained in:
commit
a505ddb927
11 changed files with 281 additions and 69 deletions
|
|
@ -60492,7 +60492,7 @@
|
|||
"paths": {
|
||||
"/v1/vector_store/list": {
|
||||
"get": {
|
||||
"description": "List all available vector stores with optional filtering and pagination.\nCombines both in-memory vector stores and those stored in the database.\nDatabase is the source of truth for stores it owns: deleted stores are removed from memory, updated stores\nsync to memory. Stores declared in the config file are owned by the config file, are always listed, and are\nnever overwritten by database rows.\n\nParameters:\n- page: int - Page number for pagination (default: 1)\n- page_size: int - Number of items per page (default: 100)",
|
||||
"description": "List all available vector stores with optional filtering and pagination.\nCombines both in-memory vector stores and those stored in the database.\nDatabase is the source of truth for stores it owns: deleted stores are removed from memory, updated stores\nsync to memory. Stores declared in the config file are owned by the config file, remain in the catalog, and\nare never overwritten by database rows. Accessible stores are ordered by vector_store_id before pagination.\n\nParameters:\n- page: int - Positive page number for pagination (default: 1)\n- page_size: int - Positive number of items per page (default: 100)",
|
||||
"operationId": "list_vector_stores_v1_vector_store_list_get",
|
||||
"parameters": [
|
||||
{
|
||||
|
|
@ -60643,7 +60643,7 @@
|
|||
},
|
||||
"/vector_store/list": {
|
||||
"get": {
|
||||
"description": "List all available vector stores with optional filtering and pagination.\nCombines both in-memory vector stores and those stored in the database.\nDatabase is the source of truth for stores it owns: deleted stores are removed from memory, updated stores\nsync to memory. Stores declared in the config file are owned by the config file, are always listed, and are\nnever overwritten by database rows.\n\nParameters:\n- page: int - Page number for pagination (default: 1)\n- page_size: int - Number of items per page (default: 100)",
|
||||
"description": "List all available vector stores with optional filtering and pagination.\nCombines both in-memory vector stores and those stored in the database.\nDatabase is the source of truth for stores it owns: deleted stores are removed from memory, updated stores\nsync to memory. Stores declared in the config file are owned by the config file, remain in the catalog, and\nare never overwritten by database rows. Accessible stores are ordered by vector_store_id before pagination.\n\nParameters:\n- page: int - Positive page number for pagination (default: 1)\n- page_size: int - Positive number of items per page (default: 100)",
|
||||
"operationId": "list_vector_stores_vector_store_list_get",
|
||||
"parameters": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -365,13 +365,18 @@ async def list_vector_stores(
|
|||
List all available vector stores with optional filtering and pagination.
|
||||
Combines both in-memory vector stores and those stored in the database.
|
||||
Database is the source of truth for stores it owns: deleted stores are removed from memory, updated stores
|
||||
sync to memory. Stores declared in the config file are owned by the config file, are always listed, and are
|
||||
never overwritten by database rows.
|
||||
sync to memory. Stores declared in the config file are owned by the config file, remain in the catalog, and
|
||||
are never overwritten by database rows. Accessible stores are ordered by vector_store_id before pagination.
|
||||
|
||||
Parameters:
|
||||
- page: int - Page number for pagination (default: 1)
|
||||
- page_size: int - Number of items per page (default: 100)
|
||||
- page: int - Positive page number for pagination (default: 1)
|
||||
- page_size: int - Positive number of items per page (default: 100)
|
||||
"""
|
||||
if page < 1:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"page must be >= 1, got page={page}",
|
||||
)
|
||||
if page_size < 1:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -434,19 +439,24 @@ async def list_vector_stores(
|
|||
)
|
||||
|
||||
# Filter vector stores based on access control
|
||||
accessible_vector_stores: Final = []
|
||||
for vs in await filter_listable_vector_stores(vector_store_map.values(), user_api_key_dict):
|
||||
redacted = _with_ownership(vs)
|
||||
redacted["litellm_params"] = _redact_sensitive_litellm_params(vs.get("litellm_params"))
|
||||
accessible_vector_stores.append(redacted)
|
||||
|
||||
accessible_vector_stores: Final = tuple(
|
||||
sorted(
|
||||
await filter_listable_vector_stores(vector_store_map.values(), user_api_key_dict),
|
||||
key=lambda store: store.get("vector_store_id", ""),
|
||||
)
|
||||
)
|
||||
total_count: Final = len(accessible_vector_stores)
|
||||
total_pages: Final = (total_count + page_size - 1) // page_size
|
||||
page_start: Final = (page - 1) * page_size
|
||||
|
||||
# Format response using LiteLLM_ManagedVectorStoreListResponse
|
||||
response: Final = LiteLLM_ManagedVectorStoreListResponse(
|
||||
object="list",
|
||||
data=accessible_vector_stores,
|
||||
data=[
|
||||
_with_ownership(vs)
|
||||
| LiteLLM_ManagedVectorStore(litellm_params=_redact_sensitive_litellm_params(vs.get("litellm_params")))
|
||||
for vs in accessible_vector_stores[page_start : page_start + page_size]
|
||||
],
|
||||
total_count=total_count,
|
||||
current_page=page,
|
||||
total_pages=total_pages,
|
||||
|
|
|
|||
|
|
@ -67,22 +67,42 @@ def assert_config_write_refused(gateway: Gateway) -> None:
|
|||
assert "config file" in str(error["error"]), refused.text
|
||||
|
||||
|
||||
def test_list_page_zero_returns_same_stores_as_page_one_and_page_size_zero_is_400(gateway: Gateway) -> None:
|
||||
page_one: Final = gateway.request("GET", "/vector_store/list?page=1&page_size=100")
|
||||
@pytest.mark.parametrize("path", ["/vector_store/list", "/v1/vector_store/list"])
|
||||
@pytest.mark.parametrize(("parameter", "value"), [("page", 0), ("page", -1), ("page_size", 0), ("page_size", -1)])
|
||||
def test_list_rejects_non_positive_pagination(gateway: Gateway, path: str, parameter: str, value: int) -> None:
|
||||
page_one: Final = gateway.request("GET", f"{path}?page=1&page_size=100")
|
||||
assert page_one.status_code == 200, page_one.text
|
||||
page_zero: Final = gateway.request("GET", "/vector_store/list?page=0&page_size=100")
|
||||
assert page_zero.status_code == 200, page_zero.text
|
||||
|
||||
page_one_ids: Final = {str(row["vector_store_id"]) for row in listed_rows(page_one)}
|
||||
page_zero_ids: Final = {str(row["vector_store_id"]) for row in listed_rows(page_zero)}
|
||||
assert page_one_ids == page_zero_ids
|
||||
assert CONFIG_STORE_ID in page_one_ids
|
||||
assert CONFIG_STORE_ID in page_zero_ids
|
||||
assert object_value(page_zero.json())["current_page"] == 0, page_zero.text
|
||||
|
||||
zero_page_size: Final = gateway.request("GET", "/vector_store/list?page=1&page_size=0")
|
||||
assert zero_page_size.status_code == 400, zero_page_size.text
|
||||
assert "page_size must be >= 1" in zero_page_size.text, zero_page_size.text
|
||||
invalid: Final = gateway.request("GET", f"{path}?{parameter}={value}")
|
||||
assert invalid.status_code == 400, invalid.text
|
||||
assert f"{parameter} must be >= 1" in invalid.text, invalid.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["/vector_store/list", "/v1/vector_store/list"])
|
||||
def test_list_pages_cover_config_and_database_stores_once(gateway: Gateway, path: str) -> None:
|
||||
initial: Final = gateway.request("GET", f"{path}?page_size=100")
|
||||
assert initial.status_code == 200, initial.text
|
||||
initial_ids: Final = tuple(str(row["vector_store_id"]) for row in listed_rows(initial))
|
||||
assert len(initial_ids) == object_value(initial.json())["total_count"], initial.text
|
||||
|
||||
with gateway.scenario() as scenario:
|
||||
prefix: Final = f"vs_pagination_{uuid.uuid4().hex}"
|
||||
created_ids: Final = tuple(f"{prefix}_{suffix}" for suffix in ("c", "a", "b"))
|
||||
for vector_store_id in created_ids:
|
||||
gateway.post("/vector_store/new", {"vector_store_id": vector_store_id, "custom_llm_provider": "openai"})
|
||||
scenario.cleanups.callback(gateway.post, "/vector_store/delete", {"vector_store_id": vector_store_id})
|
||||
|
||||
expected_ids: Final = sorted((*initial_ids, *created_ids))
|
||||
total_pages: Final = (len(expected_ids) + 1) // 2
|
||||
for page in range(1, total_pages + 2):
|
||||
response = gateway.request("GET", f"{path}?page={page}&page_size=2")
|
||||
assert response.status_code == 200, response.text
|
||||
body = object_value(response.json())
|
||||
assert [row["vector_store_id"] for row in listed_rows(response)] == expected_ids[(page - 1) * 2 : page * 2]
|
||||
assert body["object"] == "list" and body["current_page"] == page, response.text
|
||||
assert body["total_count"] == len(expected_ids) and body["total_pages"] == total_pages, response.text
|
||||
|
||||
|
||||
def burst_list(gateway: Gateway) -> tuple[int, str]:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Final
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -14,7 +15,7 @@ from litellm.llms.base_llm.vector_store.transformation import (
|
|||
LiteLLMVectorStoreEmbeddingExecutor,
|
||||
RouterVectorStoreEmbeddingExecutor,
|
||||
)
|
||||
from litellm.proxy._types import CommonProxyErrors, LitellmUserRoles, UserAPIKeyAuth
|
||||
from litellm.proxy._types import CommonProxyErrors, LiteLLM_ObjectPermissionTable, LitellmUserRoles, UserAPIKeyAuth
|
||||
from litellm.proxy.vector_store_endpoints.endpoints import (
|
||||
_update_request_data_with_litellm_managed_vector_store_registry,
|
||||
index_create,
|
||||
|
|
@ -23,6 +24,7 @@ from litellm.proxy.vector_store_endpoints.endpoints import (
|
|||
from litellm.proxy.vector_store_endpoints.management_endpoints import (
|
||||
_check_vector_store_access,
|
||||
create_vector_store_in_db,
|
||||
list_vector_stores,
|
||||
new_vector_store,
|
||||
)
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
|
|
@ -36,6 +38,7 @@ from litellm.proxy.vector_store_files_endpoints.endpoints import (
|
|||
from litellm.types.utils import EmbeddingResponse, LlmProviders
|
||||
from litellm.types.vector_stores import IndexCreateRequest, IndexListResponse
|
||||
from litellm.vector_stores.main import _direct_vector_store_embedding_executor
|
||||
from litellm.vector_stores.vector_store_registry import VectorStoreRegistry
|
||||
|
||||
|
||||
def _serialize_litellm_params(litellm_params):
|
||||
|
|
@ -3118,7 +3121,10 @@ class TestConfigOwnedVectorStores:
|
|||
first = await list_vector_stores(user_api_key_dict=self._admin())
|
||||
second = await list_vector_stores(user_api_key_dict=self._admin())
|
||||
|
||||
assert [(vs["vector_store_id"], vs["is_config"]) for vs in first["data"]] == [(self.DB_ID, False), (self.CONFIG_ID, True)]
|
||||
assert [(vs["vector_store_id"], vs["is_config"]) for vs in first["data"]] == [
|
||||
(self.CONFIG_ID, True),
|
||||
(self.DB_ID, False),
|
||||
]
|
||||
assert second["data"] == first["data"]
|
||||
assert [vs["vector_store_id"] for vs in registry.vector_stores] == [self.CONFIG_ID, self.DB_ID]
|
||||
|
||||
|
|
@ -3413,3 +3419,81 @@ def test_proxy_vector_store_writes_send_only_the_callers_metadata_when_the_raw_b
|
|||
assert response.status_code == 200, response.text
|
||||
assert len(sent_bodies) == 1
|
||||
assert sent_bodies[0].get("metadata") == CALLERS_METADATA
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("store_ids", "page", "expected_ids", "total_pages"),
|
||||
[
|
||||
(("vs_c", "vs_a", "vs_b"), 1, ("vs_a", "vs_b"), 2),
|
||||
(("vs_b", "vs_c", "vs_a"), 1, ("vs_a", "vs_b"), 2),
|
||||
(("vs_c", "vs_a", "vs_b"), 2, ("vs_c",), 2),
|
||||
(("vs_c", "vs_a", "vs_b"), 3, (), 2),
|
||||
(("vs_b", "vs_a"), 1, ("vs_a", "vs_b"), 1),
|
||||
(("vs_b", "vs_a"), 2, (), 1),
|
||||
((), 1, (), 0),
|
||||
],
|
||||
)
|
||||
async def test_list_vector_stores_returns_the_requested_page_in_id_order(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
store_ids: tuple[str, ...],
|
||||
page: int,
|
||||
expected_ids: tuple[str, ...],
|
||||
total_pages: int,
|
||||
) -> None:
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
stores: Final = [
|
||||
LiteLLM_ManagedVectorStore(vector_store_id=identifier, custom_llm_provider="openai", is_config=True)
|
||||
for identifier in store_ids
|
||||
]
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", None)
|
||||
monkeypatch.setattr(litellm, "vector_store_registry", VectorStoreRegistry(vector_stores=stores))
|
||||
admin: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||
|
||||
response: Final = await list_vector_stores(user_api_key_dict=admin, page=page, page_size=2)
|
||||
|
||||
assert response == {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{"vector_store_id": identifier, "custom_llm_provider": "openai", "is_config": True, "litellm_params": None}
|
||||
for identifier in expected_ids
|
||||
],
|
||||
"total_count": len(store_ids),
|
||||
"current_page": page,
|
||||
"total_pages": total_pages,
|
||||
}
|
||||
assert tuple(store["vector_store_id"] for store in stores) == store_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_vector_stores_paginates_only_the_stores_granted_to_the_key(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
stores: Final = [
|
||||
LiteLLM_ManagedVectorStore(vector_store_id=identifier, custom_llm_provider="openai", is_config=True)
|
||||
for identifier in ("vs_c", "vs_b", "vs_a")
|
||||
]
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", None)
|
||||
monkeypatch.setattr(proxy_server, "general_settings", {})
|
||||
monkeypatch.setattr(litellm, "vector_store_registry", VectorStoreRegistry(vector_stores=stores))
|
||||
user: Final = UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
object_permission=LiteLLM_ObjectPermissionTable(
|
||||
object_permission_id="op-paged-catalog", vector_stores=["vs_a", "vs_c"]
|
||||
),
|
||||
)
|
||||
|
||||
response: Final = await list_vector_stores(user_api_key_dict=user, page=2, page_size=1)
|
||||
|
||||
assert response == {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{"vector_store_id": "vs_c", "custom_llm_provider": "openai", "is_config": True, "litellm_params": None}
|
||||
],
|
||||
"total_count": 2,
|
||||
"current_page": 2,
|
||||
"total_pages": 2,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,30 +142,26 @@ async def test_list_vector_stores_rejects_non_positive_page_size_with_400(page_s
|
|||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("page", [0, -1])
|
||||
async def test_list_vector_stores_accepts_non_positive_page_like_base(page):
|
||||
async def test_list_vector_stores_rejects_non_positive_page_with_400(
|
||||
page: int, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
from litellm.proxy.vector_store_endpoints.management_endpoints import (
|
||||
list_vector_stores,
|
||||
)
|
||||
|
||||
import litellm
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
admin: Final = UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN.value,
|
||||
user_id="admin-1",
|
||||
)
|
||||
|
||||
mock_prisma: Final = MagicMock()
|
||||
mock_prisma.db.litellm_managedvectorstorestable.find_many = AsyncMock(return_value=[])
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", None)
|
||||
monkeypatch.setattr(litellm, "vector_store_registry", None)
|
||||
|
||||
with patch.dict("litellm.proxy.proxy_server.general_settings", _DISABLED_GS, clear=True):
|
||||
with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma):
|
||||
with patch.object(litellm, "vector_store_registry", None):
|
||||
with patch(
|
||||
"litellm.proxy.vector_store_endpoints.management_endpoints.VectorStoreRegistry.get_vector_stores_from_db",
|
||||
new=AsyncMock(return_value=[]),
|
||||
):
|
||||
response: Final = await list_vector_stores(user_api_key_dict=admin, page=page, page_size=10)
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await list_vector_stores(user_api_key_dict=admin, page=page, page_size=10)
|
||||
|
||||
assert response["current_page"] == page
|
||||
assert response["total_count"] == 0
|
||||
assert response["data"] == []
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.detail == f"page must be >= 1, got page={page}"
|
||||
|
|
|
|||
|
|
@ -1754,10 +1754,10 @@
|
|||
"count": 5
|
||||
},
|
||||
"no-restricted-syntax": {
|
||||
"count": 139
|
||||
"count": 138
|
||||
},
|
||||
"prefer-const": {
|
||||
"count": 28
|
||||
"count": 27
|
||||
}
|
||||
},
|
||||
"src/components/object_permissions_view.tsx": {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ vi.mock("../networking", async () => {
|
|||
guardrails: [{ guardrail_name: "test-guardrail-1" }, { guardrail_name: "test-guardrail-2" }],
|
||||
}),
|
||||
tagListCall: vi.fn().mockResolvedValue({}),
|
||||
vectorStoreListCall: vi.fn().mockResolvedValue({ data: [] }),
|
||||
modelAvailableCall: vi.fn().mockResolvedValue({
|
||||
data: [{ id: "model-group-1" }, { id: "model-group-2" }],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ vi.mock("@/app/(dashboard)/hooks/uiSettings/usePtuCostAttributionEnabled", () =>
|
|||
usePtuCostAttributionEnabled: () => mockUsePtuCostAttributionEnabled(),
|
||||
}));
|
||||
|
||||
// The real selector requests /vector_store/list from the proxy as soon as it mounts
|
||||
vi.mock("@/components/vector_store_management/VectorStoreSelector", () => ({
|
||||
default: () => <div data-testid="vector-store-selector" />,
|
||||
}));
|
||||
|
||||
const PTU_LABELS = ["PTU Count", "Calculated Cost per PTU / Hour (USD)", "PTU Effective From (UTC)"];
|
||||
|
||||
const KubernetesPodDiscoveryValue = () => {
|
||||
|
|
|
|||
|
|
@ -1020,6 +1020,93 @@ describe("ragIngestCall", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("vectorStoreListCall complete dashboard inventory", () => {
|
||||
const stores = Array.from({ length: 203 }, (_, index) => ({
|
||||
vector_store_id: `vs_${String(index).padStart(3, "0")}`,
|
||||
custom_llm_provider: "openai",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
Networking.setGlobalLitellmHeaderName("Authorization");
|
||||
});
|
||||
|
||||
it("loads every page with the configured authentication header", async () => {
|
||||
const requests: { path: string; page: string | null; pageSize: string | null; auth: string | null }[] = [];
|
||||
const mockFetch = vi.fn<typeof fetch>().mockImplementation(async (input, init) => {
|
||||
const url = new URL(input instanceof Request ? input.url : String(input), "http://localhost");
|
||||
const request = {
|
||||
path: url.pathname,
|
||||
page: url.searchParams.get("page"),
|
||||
pageSize: url.searchParams.get("page_size"),
|
||||
auth: new Headers(init?.headers).get("x-litellm-key"),
|
||||
};
|
||||
requests.push(request);
|
||||
const page = requests.length;
|
||||
const inventory = {
|
||||
object: "list",
|
||||
data: stores.slice((page - 1) * 100, page * 100),
|
||||
current_page: page,
|
||||
total_count: stores.length,
|
||||
total_pages: 3,
|
||||
};
|
||||
return new Response(JSON.stringify(inventory));
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
Networking.setGlobalLitellmHeaderName("x-litellm-key");
|
||||
|
||||
const response = await Networking.vectorStoreListCall("inventory-token");
|
||||
|
||||
expect(response.data).toEqual(stores);
|
||||
expect(requests).toEqual([
|
||||
{ path: "/vector_store/list", page: "1", pageSize: "100", auth: "Bearer inventory-token" },
|
||||
{ path: "/vector_store/list", page: "2", pageSize: "100", auth: "Bearer inventory-token" },
|
||||
{ path: "/vector_store/list", page: "3", pageSize: "100", auth: "Bearer inventory-token" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not duplicate an older backend's already-complete first response", async () => {
|
||||
const inventory = { object: "list", data: stores, total_count: stores.length, current_page: 1, total_pages: 3 };
|
||||
const mockFetch = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify(inventory)));
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const response = await Networking.vectorStoreListCall("inventory-token");
|
||||
|
||||
expect(response.data).toEqual(stores);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps an empty inventory empty without requesting another page", async () => {
|
||||
const inventory = { object: "list", data: [], total_count: 0, current_page: 1, total_pages: 0 };
|
||||
const mockFetch = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify(inventory)));
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const response = await Networking.vectorStoreListCall("inventory-token");
|
||||
|
||||
expect(response.data).toEqual([]);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reports a later page error instead of returning a partial inventory", async () => {
|
||||
const inventory = {
|
||||
object: "list",
|
||||
data: stores.slice(0, 100),
|
||||
total_count: stores.length,
|
||||
current_page: 1,
|
||||
total_pages: 3,
|
||||
};
|
||||
const mockFetch = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify(inventory)))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ detail: "Catalog page unavailable" }), { status: 503 }));
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
await expect(Networking.vectorStoreListCall("inventory-token")).rejects.toThrow("Catalog page unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUiConfig runtime backend discovery", () => {
|
||||
const discoveryConfig = () => ({
|
||||
server_root_path: "/",
|
||||
|
|
@ -1177,6 +1264,19 @@ describe("getUiConfig runtime backend discovery", () => {
|
|||
expect(requestUrl.pathname).toBe("/rag/ingest");
|
||||
});
|
||||
|
||||
it("keeps vector store inventory on the management backend", async () => {
|
||||
const { networking, mockFetch } = await loadNetworkingWithGateway("https://gateway.example.com");
|
||||
const inventory = { object: "list", data: [], total_count: 0, current_page: 1, total_pages: 0 };
|
||||
mockFetch.mockResolvedValueOnce(new Response(JSON.stringify(inventory)));
|
||||
|
||||
const result = await networking.vectorStoreListCall("token");
|
||||
|
||||
expect(result.data).toEqual([]);
|
||||
const requestUrl = new URL(String(mockFetch.mock.calls.at(-1)?.[0]));
|
||||
expect(requestUrl.origin).toBe("https://backend.example.com");
|
||||
expect(requestUrl.pathname).toBe("/vector_store/list");
|
||||
});
|
||||
|
||||
it("lists indexes from the gateway", async () => {
|
||||
const { networking, mockFetch } = await loadNetworkingWithGateway("https://gateway.example.com");
|
||||
await networking.indexesListCall("token");
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ import { MCP_TOOLS_PREVIEW_FORBIDDEN_MESSAGE } from "./mcp_tools/constants";
|
|||
import type { ComplexityRouterConfigPayload } from "./add_model/build_complexity_router_config";
|
||||
import type { AutoRouterPresetsResponse } from "@/lib/autorouter_presets";
|
||||
import type { VectorStoreIndex } from "@/app/(dashboard)/vector-stores/_components/IndexesTab";
|
||||
import type { VectorStoreListResponse } from "./vector_store_management/types";
|
||||
import type { RoutingDecision } from "./logs/detail/RoutingDecisionCard";
|
||||
import type {
|
||||
SpanDetail,
|
||||
|
|
@ -5550,28 +5551,23 @@ export const vectorStoreCreateCall = async (accessToken: string, formValues: Rec
|
|||
}
|
||||
};
|
||||
|
||||
export const vectorStoreListCall = async (
|
||||
accessToken: string,
|
||||
page: number = 1,
|
||||
page_size: number = 100,
|
||||
): Promise<any> => {
|
||||
export const vectorStoreListCall = async (accessToken: string): Promise<Pick<VectorStoreListResponse, "data">> => {
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/vector_store/list` : `/vector_store/list`;
|
||||
const fetchPage = (page: number) =>
|
||||
apiClient.get<VectorStoreListResponse>("/vector_store/list", {
|
||||
accessToken,
|
||||
query: { page, page_size: 100 },
|
||||
});
|
||||
const firstPage = await fetchPage(1);
|
||||
const data = [...firstPage.data];
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || "Failed to list vector stores");
|
||||
// Dashboard tables and selectors need the full catalog. Older gateways may already return it on page one.
|
||||
for (let page = 2; page <= firstPage.total_pages && data.length < firstPage.total_count; page++) {
|
||||
const response = await fetchPage(page);
|
||||
data.push(...response.data);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
return { data };
|
||||
} catch (error) {
|
||||
console.error("Error listing vector stores:", error);
|
||||
throw error;
|
||||
|
|
|
|||
16
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
16
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -22817,12 +22817,12 @@ export interface paths {
|
|||
* @description List all available vector stores with optional filtering and pagination.
|
||||
* Combines both in-memory vector stores and those stored in the database.
|
||||
* Database is the source of truth for stores it owns: deleted stores are removed from memory, updated stores
|
||||
* sync to memory. Stores declared in the config file are owned by the config file, are always listed, and are
|
||||
* never overwritten by database rows.
|
||||
* sync to memory. Stores declared in the config file are owned by the config file, remain in the catalog, and
|
||||
* are never overwritten by database rows. Accessible stores are ordered by vector_store_id before pagination.
|
||||
*
|
||||
* Parameters:
|
||||
* - page: int - Page number for pagination (default: 1)
|
||||
* - page_size: int - Number of items per page (default: 100)
|
||||
* - page: int - Positive page number for pagination (default: 1)
|
||||
* - page_size: int - Positive number of items per page (default: 100)
|
||||
*/
|
||||
get: operations["list_vector_stores_v1_vector_store_list_get"];
|
||||
put?: never;
|
||||
|
|
@ -24125,12 +24125,12 @@ export interface paths {
|
|||
* @description List all available vector stores with optional filtering and pagination.
|
||||
* Combines both in-memory vector stores and those stored in the database.
|
||||
* Database is the source of truth for stores it owns: deleted stores are removed from memory, updated stores
|
||||
* sync to memory. Stores declared in the config file are owned by the config file, are always listed, and are
|
||||
* never overwritten by database rows.
|
||||
* sync to memory. Stores declared in the config file are owned by the config file, remain in the catalog, and
|
||||
* are never overwritten by database rows. Accessible stores are ordered by vector_store_id before pagination.
|
||||
*
|
||||
* Parameters:
|
||||
* - page: int - Page number for pagination (default: 1)
|
||||
* - page_size: int - Number of items per page (default: 100)
|
||||
* - page: int - Positive page number for pagination (default: 1)
|
||||
* - page_size: int - Positive number of items per page (default: 100)
|
||||
*/
|
||||
get: operations["list_vector_stores_vector_store_list_get"];
|
||||
put?: never;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue