[Feat] Vector Stores/KnowledgeBases - Allow defining Vector Store Configs (#10448)

* init vector store configs

* working kb init

* add vector store endpoints

* use litellm_credential_name

* working CRUD vector stores litellm

* working creds with vector DB

* ui cleanup

* clean up vector store id

* fix delete button

* refactored vector store component

* working selector for KBs

* ui vector stores

* add vector store tool calls usage on chat ui

* fixes for vector stores litellm

* test fix

* docs Knowledge Bases

* fixes for vector stores litellm

* fix linting

* add managed vectorstores

* fix orjson ci/cd test

* fix linting

* add types.tsx file
This commit is contained in:
Ishaan Jaff 2025-04-30 21:49:59 -07:00 • committed by GitHub
parent 7c679abe85
commit be885e4a17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1716 additions and 41 deletions

View file

@ -1,10 +1,10 @@
# Using Knowledge Bases with LiteLLM
# Using Vector Stores (Knowledge Bases) with LiteLLM
LiteLLM integrates with AWS Bedrock Knowledge Bases, allowing your models to access your organization's data for more accurate and contextually relevant responses.
## Quick Start
In order to use a Bedrock Knowledge Base with LiteLLM, you need to pass `knowledge_bases` as a parameter to the completion request. Where `knowledge_bases` is a list of Bedrock Knowledge Base IDs.
In order to use a Bedrock Knowledge Base with LiteLLM, you need to pass `vector_store_ids` as a parameter to the completion request. Where `vector_store_ids` is a list of Bedrock Knowledge Base IDs.
### LiteLLM Python SDK
@ -13,11 +13,11 @@ import os
import litellm
# Make a completion request with knowledge_bases parameter
# Make a completion request with vector_store_ids parameter
response = await litellm.acompletion(
model="anthropic/claude-3-5-sonnet",
messages=[{"role": "user", "content": "What is litellm?"}],
knowledge_bases=["YOUR_KNOWLEDGE_BASE_ID"] # e.g., "T37J8R4WTM"
vector_store_ids=["YOUR_KNOWLEDGE_BASE_ID"] # e.g., "T37J8R4WTM"
)
print(response.choices[0].message.content)
@ -36,7 +36,7 @@ model_list:
```
#### 2. Make a request with knowledge_bases parameter
#### 2. Make a request with vector_store_ids parameter
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
@ -51,7 +51,7 @@ curl http://localhost:4000/v1/chat/completions \
-d '{
"model": "claude-3-5-sonnet",
"messages": [{"role": "user", "content": "What is litellm?"}],
"knowledge_bases": ["YOUR_KNOWLEDGE_BASE_ID"]
"vector_store_ids": ["YOUR_KNOWLEDGE_BASE_ID"]
}'
```
@ -68,11 +68,11 @@ client = OpenAI(
api_key="your-litellm-api-key"
)
# Make a completion request with knowledge_bases parameter
# Make a completion request with vector_store_ids parameter
response = client.chat.completions.create(
model="claude-3-5-sonnet",
messages=[{"role": "user", "content": "What is litellm?"}],
extra_body={"knowledge_bases": ["YOUR_KNOWLEDGE_BASE_ID"]}
extra_body={"vector_store_ids": ["YOUR_KNOWLEDGE_BASE_ID"]}
)
print(response.choices[0].message.content)
@ -85,7 +85,7 @@ print(response.choices[0].message.content)
LiteLLM implements a `BedrockKnowledgeBaseHook` that intercepts your completion requests for handling the integration with Bedrock Knowledge Bases.
1. You make a completion request with the `knowledge_bases` parameter
1. You make a completion request with the `vector_store_ids` parameter
2. LiteLLM automatically:
- Uses your last message as the query to retrieve relevant information from the Knowledge Base
- Adds the retrieved context to your conversation
@ -93,7 +93,7 @@ LiteLLM implements a `BedrockKnowledgeBaseHook` that intercepts your completion
### Example Transformation
When you pass `knowledge_bases=["YOUR_KNOWLEDGE_BASE_ID"]`, your request flows through these steps:
When you pass `vector_store_ids=["YOUR_KNOWLEDGE_BASE_ID"]`, your request flows through these steps:
**1. Original Request to LiteLLM:**
```json
@ -102,7 +102,7 @@ When you pass `knowledge_bases=["YOUR_KNOWLEDGE_BASE_ID"]`, your request flows t
"messages": [
{"role": "user", "content": "What is litellm?"}
],
"knowledge_bases": ["YOUR_KNOWLEDGE_BASE_ID"]
"vector_store_ids": ["YOUR_KNOWLEDGE_BASE_ID"]
}
```
@ -127,7 +127,7 @@ This is sent to: `https://bedrock-agent-runtime.{aws_region}.amazonaws.com/knowl
}
```
This process happens automatically whenever you include the `knowledge_bases` parameter in your request.
This process happens automatically whenever you include the `vector_store_ids` parameter in your request.
## API Reference
@ -137,4 +137,4 @@ When using the Knowledge Base integration with LiteLLM, you can include the foll
| Parameter | Type | Description |
|-----------|------|-------------|
| `knowledge_bases` | List[str] | List of Bedrock Knowledge Base IDs to query |
| `vector_store_ids` | List[str] | List of Bedrock Knowledge Base IDs to query |

View file

@ -1,8 +1,10 @@
from fastapi import APIRouter
from fastapi.responses import Response
from .utils import _should_block_robots
from .vector_stores.endpoints import router as vector_stores_router
router = APIRouter()
router.include_router(vector_stores_router)
@router.get("/robots.txt")
@ -14,4 +16,4 @@ async def get_robots():
if _should_block_robots():
return Response(content="User-agent: *\nDisallow: /", media_type="text/plain")
else:
return Response(status_code=404)
return Response(status_code=404)

View file

@ -0,0 +1,180 @@
"""
VECTOR STORE MANAGEMENT
All /vector_store management endpoints
/vector_store/new
/vector_store/delete
/vector_store/list
"""
import copy
from typing import List
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from fastapi import APIRouter, Depends, HTTPException
from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.vector_stores.vector_store_registry import global_vector_store_manager
from litellm.types.vector_stores import (
LiteLLM_ManagedVectorStoreListResponse,
LiteLLM_ManagedVectorStore,
VectorStoreDeleteRequest,
)
router = APIRouter()
@router.post(
"/vector_store/new",
tags=["vector store management"],
dependencies=[Depends(user_api_key_auth)],
)
async def new_vector_store(
vector_store: LiteLLM_ManagedVectorStore,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Create a new vector store.
Parameters:
- vector_store_id: str - Unique identifier for the vector store
- custom_llm_provider: str - Provider of the vector store
- vector_store_name: Optional[str] - Name of the vector store
- vector_store_description: Optional[str] - Description of the vector store
- vector_store_metadata: Optional[Dict] - Additional metadata for the vector store
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
# Check if vector store already exists
existing_vector_store = await prisma_client.db.litellm_managedvectorstorestable.find_unique(
where={"vector_store_id": vector_store.get("vector_store_id")}
)
if existing_vector_store is not None:
raise HTTPException(
status_code=400,
detail=f"Vector store with ID {vector_store.get('vector_store_id')} already exists"
)
if vector_store.get("vector_store_metadata") is not None:
vector_store["vector_store_metadata"] = safe_dumps(vector_store.get("vector_store_metadata"))
new_vector_store = await prisma_client.db.litellm_managedvectorstorestable.create(
data=vector_store
)
return {
"status": "success",
"message": f"Vector store {vector_store.get('vector_store_id')} created successfully",
"vector_store": new_vector_store,
}
except Exception as e:
verbose_proxy_logger.exception(f"Error creating vector store: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get(
"/vector_store/list",
tags=["vector store management"],
dependencies=[Depends(user_api_key_auth)],
response_model=LiteLLM_ManagedVectorStoreListResponse,
)
async def list_vector_stores(
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
page: int = 1,
page_size: int = 100,
):
"""
List all available vector stores with optional filtering and pagination.
Combines both in-memory vector stores and those stored in the database.
Parameters:
- page: int - Page number for pagination (default: 1)
- page_size: int - Number of items per page (default: 100)
"""
from litellm.proxy.proxy_server import prisma_client
try:
# Get in-memory vector stores
in_memory_vector_stores: List[LiteLLM_ManagedVectorStore] = copy.deepcopy(global_vector_store_manager.vector_stores)
# Get vector stores from database
vector_stores_from_db: List[LiteLLM_ManagedVectorStore] = []
if prisma_client is not None:
_vector_stores_from_db = await prisma_client.db.litellm_managedvectorstorestable.find_many(
order={
"created_at": "desc"
},
)
for vector_store in _vector_stores_from_db:
_dict_vector_store = dict(vector_store)
_litellm_managed_vector_store = LiteLLM_ManagedVectorStore(**_dict_vector_store)
vector_stores_from_db.append(_litellm_managed_vector_store)
# Combine in-memory and database vector stores
combined_vector_stores: List[LiteLLM_ManagedVectorStore] = in_memory_vector_stores + vector_stores_from_db
total_count = len(combined_vector_stores)
total_pages = (total_count + page_size - 1) // page_size
# Format response using LiteLLM_ManagedVectorStoreListResponse
response = LiteLLM_ManagedVectorStoreListResponse(
object="list",
data=combined_vector_stores,
total_count=total_count,
current_page=page,
total_pages=total_pages,
)
return response
except Exception as e:
verbose_proxy_logger.exception(f"Error listing vector stores: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/vector_store/delete",
tags=["vector store management"],
dependencies=[Depends(user_api_key_auth)],
)
async def delete_vector_store(
data: VectorStoreDeleteRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Delete a vector store.
Parameters:
- vector_store_id: str - ID of the vector store to delete
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
# Check if vector store exists
existing_vector_store = await prisma_client.db.litellm_managedvectorstorestable.find_unique(
where={"vector_store_id": data.vector_store_id}
)
if existing_vector_store is None:
raise HTTPException(
status_code=404,
detail=f"Vector store with ID {data.vector_store_id} not found"
)
# Delete vector store
await prisma_client.db.litellm_managedvectorstorestable.delete(
where={"vector_store_id": data.vector_store_id}
)
return {"message": f"Vector store {data.vector_store_id} deleted successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View file

@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "LiteLLM_ManagedVectorStoresTable" (
"vector_store_id" TEXT NOT NULL,
"custom_llm_provider" TEXT NOT NULL,
"vector_store_name" TEXT,
"vector_store_description" TEXT,
"vector_store_metadata" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"litellm_credential_name" TEXT,
CONSTRAINT "LiteLLM_ManagedVectorStoresTable_pkey" PRIMARY KEY ("vector_store_id")
);

View file

@ -425,3 +425,14 @@ model LiteLLM_ManagedFileTable {
@@index([unified_file_id])
}
model LiteLLM_ManagedVectorStoresTable {
vector_store_id String @id
custom_llm_provider String
vector_store_name String?
vector_store_description String?
vector_store_metadata Json?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
litellm_credential_name String?
}

View file

@ -62,11 +62,11 @@ class BedrockKnowledgeBaseHook(CustomPromptManagement, BaseAWSLLM):
"""
Retrieves the context from the Bedrock Knowledge Base and appends it to the messages.
"""
knowledge_bases = non_default_params.pop("knowledge_bases", None)
if knowledge_bases:
for knowledge_base in knowledge_bases:
vector_store_ids = non_default_params.pop("vector_store_ids", None)
if vector_store_ids:
for vector_store_id in vector_store_ids:
response = await self.make_bedrock_kb_retrieve_request(
knowledge_base_id=knowledge_base,
knowledge_base_id=vector_store_id,
query=self._get_kb_query_from_messages(messages),
)
verbose_logger.debug(f"Bedrock Knowledge Base Response: {response}")
@ -231,7 +231,7 @@ class BedrockKnowledgeBaseHook(CustomPromptManagement, BaseAWSLLM):
@staticmethod
def should_use_prompt_management_hook(non_default_params: Dict) -> bool:
if non_default_params.get("knowledge_bases", None):
if non_default_params.get("vector_store_ids", None):
return True
return False

View file

@ -1,11 +1,22 @@
model_list:
- model_name: computer-user-agent
- model_name: openai/gpt-4o
litellm_params:
model: azure/computer-use-preview
truncation: auto
api_key: os.environ/AZURE_RESPONSES_OPENAI_API_KEY
api_base: os.environ/AZURE_RESPONSES_OPENAI_ENDPOINT
api_version: os.environ/AZURE_RESPONSES_OPENAI_API_VERSION
general_settings:
store_prompts_in_spend_logs: true
block_robots: true
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: claude-3-5-sonnet-with-vector-store
litellm_params:
model: anthropic/claude-3-5-sonnet-latest
api_key: os.environ/ANTHROPIC_API_KEY
vector_store_ids: ["T37J8R4WTM"]
vector_stores:
- vector_store_name: "bedrock-litellm-website-knowledgebase"
litellm_params:
custom_llm_provider: "bedrock"
vector_store_id: "T37J8R4WTM"
vector_store_description: "Bedrock vector store for the Litellm website knowledgebase"
vector_store_metadata:
source: "https://www.litellm.com/docs"

View file

@ -1998,6 +1998,19 @@ class ProxyConfig:
all_guardrails=guardrails_v2, config_file_path=config_file_path
)
## CREDENTIALS
credential_list_dict = self.load_credential_list(config=config)
litellm.credential_list = credential_list_dict
## NON-LLM CONFIGS eg. MCP tools, vector stores, etc.
self._init_non_llm_configs(config=config)
return router, router.get_model_list(), general_settings
def _init_non_llm_configs(self, config: dict):
"""
Initialize non-LLM configs eg. MCP tools, vector stores, etc.
"""
## MCP TOOLS
mcp_tools_config = config.get("mcp_tools", None)
if mcp_tools_config:
@ -2011,10 +2024,17 @@ class ProxyConfig:
global_mcp_server_manager.load_servers_from_config(mcp_servers_config)
## CREDENTIALS
credential_list_dict = self.load_credential_list(config=config)
litellm.credential_list = credential_list_dict
return router, router.get_model_list(), general_settings
## VECTOR STORES
vector_stores_config = config.get("vector_stores", None)
if vector_stores_config:
from litellm.proxy.vector_stores.vector_store_registry import (
global_vector_store_manager,
)
global_vector_store_manager.load_vector_stores_from_config(
vector_stores_config
)
pass
def _load_alerting_settings(self, general_settings: dict):
"""

View file

@ -425,3 +425,14 @@ model LiteLLM_ManagedFileTable {
@@index([unified_file_id])
}
model LiteLLM_ManagedVectorStoresTable {
vector_store_id String @id
custom_llm_provider String
vector_store_name String?
vector_store_description String?
vector_store_metadata Json?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
litellm_credential_name String?
}

View file

@ -0,0 +1,79 @@
# litellm/proxy/vector_stores/vector_store_registry.py
import json
from datetime import datetime, timezone
from typing import Any, Dict, List
from litellm._logging import verbose_logger
from litellm.types.vector_stores import (
LiteLLM_ManagedVectorStore,
LiteLLM_ManagedVectorStoreListResponse,
LiteLLM_VectorStoreConfig,
)
class VectorStoreManager:
def __init__(self):
self.vector_stores: List[LiteLLM_ManagedVectorStore] = []
def load_vector_stores_from_config(self, vector_stores_config: List[Dict]):
for vector_store_config in vector_stores_config:
# cast to VectorStoreConfig
litellm_vector_store_config = LiteLLM_VectorStoreConfig(
**vector_store_config
)
vector_store_name = litellm_vector_store_config.get("vector_store_name")
vector_store_litellm_params: Dict[str, Any] = (
litellm_vector_store_config.get("litellm_params") or {}
)
vector_store_id = vector_store_litellm_params.get("vector_store_id")
if vector_store_id is None:
raise ValueError(
f"vector_store_id is required for initializing vector store, got vector_store_id={vector_store_id}"
)
custom_llm_provider = vector_store_litellm_params.get("custom_llm_provider")
if custom_llm_provider is None:
raise ValueError(
f"custom_llm_provider is required for initializing vector store, got custom_llm_provider={custom_llm_provider}"
)
litellm_managed_vector_store = LiteLLM_ManagedVectorStore(
vector_store_id=vector_store_id,
custom_llm_provider=custom_llm_provider,
vector_store_name=vector_store_name,
vector_store_description=vector_store_litellm_params.get(
"vector_store_description"
),
vector_store_metadata=vector_store_litellm_params.get(
"vector_store_metadata"
),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
self.vector_stores.append(litellm_managed_vector_store)
verbose_logger.debug(
"all loaded vector stores = %s",
json.dumps(self.vector_stores, indent=4, default=str),
)
def list_all_vector_stores(self) -> LiteLLM_ManagedVectorStoreListResponse:
"""
List all vector stores in the required format
Returns:
LiteLLM_ManagedVectorStoreListResponse: A standardized response with vector store data
"""
# Prepare the response
response = LiteLLM_ManagedVectorStoreListResponse(
object="list",
data=self.vector_stores,
total_count=len(self.vector_stores),
current_page=1,
total_pages=1,
)
return response
global_vector_store_manager = VectorStoreManager()

View file

@ -2272,6 +2272,7 @@ class DynamicPromptManagementParamLiteral(str, Enum):
CACHE_CONTROL_INJECTION_POINTS = "cache_control_injection_points"
KNOWLEDGE_BASES = "knowledge_bases"
VECTOR_STORE_IDS = "vector_store_ids"
@classmethod
def list_all_params(cls):

View file

@ -0,0 +1,63 @@
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, Union
from pydantic import BaseModel
from typing_extensions import TypedDict
from litellm.types.router import CredentialLiteLLMParams
class SupportedVectorStoreIntegrations(str, Enum):
"""Supported vector store integrations."""
BEDROCK = "bedrock"
class LiteLLM_VectorStoreConfig(TypedDict, total=False):
"""Parameters for initializing a vector store on Litellm proxy config.yaml"""
vector_store_name: str
litellm_params: Optional[Dict[str, Any]]
class LiteLLM_ManagedVectorStore(TypedDict, total=False):
"""LiteLLM managed vector store object - this is is the object stored in the database"""
vector_store_id: str
custom_llm_provider: str
vector_store_name: Optional[str]
vector_store_description: Optional[str]
vector_store_metadata: Optional[Union[Dict[str, Any], str]]
created_at: Optional[datetime]
updated_at: Optional[datetime]
# credential fields
litellm_credential_name: Optional[str]
class LiteLLM_ManagedVectorStoreListResponse(TypedDict, total=False):
"""Response format for listing vector stores"""
object: Literal["list"] # Always "list"
data: List[LiteLLM_ManagedVectorStore]
total_count: Optional[int]
current_page: Optional[int]
total_pages: Optional[int]
class VectorStoreUpdateRequest(BaseModel):
vector_store_id: str
custom_llm_provider: Optional[str] = None
vector_store_name: Optional[str] = None
vector_store_description: Optional[str] = None
vector_store_metadata: Optional[Dict] = None
class VectorStoreDeleteRequest(BaseModel):
vector_store_id: str
class VectorStoreInfoRequest(BaseModel):
vector_store_id: str

View file

@ -425,3 +425,14 @@ model LiteLLM_ManagedFileTable {
@@index([unified_file_id])
}
model LiteLLM_ManagedVectorStoresTable {
vector_store_id String @id
custom_llm_provider String
vector_store_name String?
vector_store_description String?
vector_store_metadata Json?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
litellm_credential_name String?
}

View file

@ -49,7 +49,7 @@ async def test_e2e_bedrock_knowledgebase_retrieval_with_completion():
response = await litellm.acompletion(
model="anthropic/claude-3.5-sonnet",
messages=[{"role": "user", "content": "what is litellm?"}],
knowledge_bases = [
vector_store_ids = [
"T37J8R4WTM"
],
client=client
@ -90,7 +90,7 @@ async def test_e2e_bedrock_knowledgebase_retrieval_with_llm_api_call():
response = await litellm.acompletion(
model="anthropic/claude-3-5-haiku-latest",
messages=[{"role": "user", "content": "what is litellm?"}],
knowledge_bases = [
vector_store_ids = [
"T37J8R4WTM"
],
client=async_client
@ -116,7 +116,7 @@ async def test_openai_with_knowledge_base_mock_openai():
await litellm.acompletion(
model="gpt-4",
messages=[{"role": "user", "content": "what is litellm?"}],
knowledge_bases = [
vector_store_ids = [
"T37J8R4WTM"
],
client=client,

View file

@ -34,6 +34,7 @@ import { fetchUserModels } from "@/components/create_key_button";
import { fetchTeams } from "@/components/common_components/fetch_teams";
import MCPToolsViewer from "@/components/mcp_tools";
import TagManagement from "@/components/tag_management";
import VectorStoreManagement from "@/components/vector_store_management";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { cx } from '@/lib/cva.config';
@ -408,6 +409,12 @@ export default function CreateKeyPage() {
userRole={userRole}
userID={userID}
/>
) : page == "vector-stores" ? (
<VectorStoreManagement
accessToken={accessToken}
userRole={userRole}
userID={userID}
/>
) : page == "new_usage" ? (
<NewUsagePage
userID={userID}

View file

@ -34,6 +34,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism';
import EndpointSelector from "./chat_ui/EndpointSelector";
import TagSelector from "./tag_management/TagSelector";
import VectorStoreSelector from "./vector_store_management/VectorStoreSelector";
import { determineEndpointType } from "./chat_ui/EndpointUtils";
import { MessageType } from "./chat_ui/types";
import ReasoningContent from "./chat_ui/ReasoningContent";
@ -47,7 +48,9 @@ import {
UserOutlined,
DeleteOutlined,
LoadingOutlined,
TagsOutlined
TagsOutlined,
DatabaseOutlined,
InfoCircleOutlined
} from "@ant-design/icons";
const { TextArea } = Input;
@ -83,6 +86,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
const [isLoading, setIsLoading] = useState<boolean>(false);
const abortControllerRef = useRef<AbortController | null>(null);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [selectedVectorStores, setSelectedVectorStores] = useState<string[]>([]);
const [messageTraceId, setMessageTraceId] = useState<string | null>(null);
const chatEndRef = useRef<HTMLDivElement>(null);
@ -328,7 +332,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
updateReasoningContent,
updateTimingData,
updateUsageData,
traceId
traceId,
selectedVectorStores.length > 0 ? selectedVectorStores : undefined
);
} else if (endpointType === EndpointType.IMAGE) {
// For image generation
@ -354,7 +359,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
updateReasoningContent,
updateTimingData,
updateUsageData,
traceId
traceId,
selectedVectorStores.length > 0 ? selectedVectorStores : undefined
);
}
}
@ -503,6 +509,27 @@ const ChatUI: React.FC<ChatUIProps> = ({
accessToken={accessToken || ""}
/>
</div>
<div>
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
<DatabaseOutlined className="mr-2" /> Vector Store
<Tooltip
className="ml-1"
title={
<span>
Select vector store(s) to use for this LLM API call. You can set up your vector store <a href="?page=vector-stores" style={{ color: '#1890ff' }}>here</a>.
</span>
}>
<InfoCircleOutlined />
</Tooltip>
</Text>
<VectorStoreSelector
value={selectedVectorStores}
onChange={setSelectedVectorStores}
className="mb-4"
accessToken={accessToken || ""}
/>
</div>
<Button
onClick={clearChatHistory}

View file

@ -13,7 +13,8 @@ export async function makeOpenAIChatCompletionRequest(
onReasoningContent?: (content: string) => void,
onTimingData?: (timeToFirstToken: number) => void,
onUsageData?: (usage: TokenUsage) => void,
traceId?: string
traceId?: string,
vector_store_ids?: string[]
) {
// base url should be the current base_url
const isLocal = process.env.NODE_ENV === "development";
@ -56,6 +57,7 @@ export async function makeOpenAIChatCompletionRequest(
},
litellm_trace_id: traceId,
messages: chatHistory as ChatCompletionMessageParam[],
...(vector_store_ids ? { vector_store_ids } : {}),
}, { signal });
for await (const chunk of response) {

View file

@ -13,7 +13,8 @@ export async function makeOpenAIResponsesRequest(
onReasoningContent?: (content: string) => void,
onTimingData?: (timeToFirstToken: number) => void,
onUsageData?: (usage: TokenUsage) => void,
traceId?: string
traceId?: string,
vector_store_ids?: string[]
) {
if (!accessToken) {
throw new Error("API key is required");
@ -60,6 +61,7 @@ export async function makeOpenAIResponsesRequest(
input: formattedInput,
stream: true,
litellm_trace_id: traceId,
...(vector_store_ids ? { vector_store_ids } : {}),
}, { signal });
for await (const event of response) {

View file

@ -73,10 +73,11 @@ const Sidebar: React.FC<SidebarProps> = ({
{ key: "9", page: "caching", label: "Caching", icon: <DatabaseOutlined />, roles: all_admin_roles },
{ key: "10", page: "budgets", label: "Budgets", icon: <BankOutlined />, roles: all_admin_roles },
{ key: "11", page: "guardrails", label: "Guardrails", icon: <SafetyOutlined />, roles: all_admin_roles },
{ key: "4", page: "usage", label: "Old Usage", icon: <BarChartOutlined /> },
{ key: "20", page: "transform-request", label: "API Playground", icon: <ApiOutlined />, roles: [...all_admin_roles, ...internalUserRoles] },
{ key: "18", page: "mcp-tools", label: "MCP Tools", icon: <ToolOutlined />, roles: all_admin_roles },
{ key: "19", page: "tag-management", label: "Tag Management", icon: <TagsOutlined />, roles: all_admin_roles },
{ key: "21", page: "vector-stores", label: "Vector Stores", icon: <DatabaseOutlined />, roles: all_admin_roles },
{ key: "4", page: "usage", label: "Old Usage", icon: <BarChartOutlined /> },
]
},
{

View file

@ -4791,4 +4791,125 @@ export const sessionSpendLogsCall = async (
console.error("Failed to fetch session logs:", error);
throw error;
}
};
export const vectorStoreCreateCall = async (
accessToken: string,
formValues: Record<string, any>
): Promise<void> => {
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/vector_store/new`
: `/vector_store/new`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(formValues)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create vector store');
}
return await response.json();
} catch (error) {
console.error('Error creating vector store:', error);
throw error;
}
};
export const vectorStoreListCall = async (
accessToken: string,
page: number = 1,
page_size: number = 100
): Promise<any> => {
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/vector_store/list`
: `/vector_store/list`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to list vector stores');
}
return await response.json();
} catch (error) {
console.error('Error listing vector stores:', error);
throw error;
}
};
export const vectorStoreDeleteCall = async (
accessToken: string,
vectorStoreId: string
): Promise<void> => {
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/vector_store/delete`
: `/vector_store/delete`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({ vector_store_id: vectorStoreId })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to delete vector store');
}
return await response.json();
} catch (error) {
console.error('Error deleting vector store:', error);
throw error;
}
};
export const vectorStoreInfoCall = async (
accessToken: string,
vectorStoreId: string
): Promise<any> => {
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/vector_store/info`
: `/vector_store/info`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({ vector_store_id: vectorStoreId })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to get vector store info');
}
return await response.json();
} catch (error) {
console.error('Error getting vector store info:', error);
throw error;
}
};

View file

@ -0,0 +1,38 @@
import React from "react";
import { Modal } from "antd";
import { Button as TremorButton } from "@tremor/react";
interface DeleteModalProps {
isVisible: boolean;
onCancel: () => void;
onConfirm: () => void;
}
const DeleteModal: React.FC<DeleteModalProps> = ({
isVisible,
onCancel,
onConfirm,
}) => {
return (
<Modal
title="Delete Vector Store"
visible={isVisible}
footer={null}
onCancel={onCancel}
>
<p>Are you sure you want to delete this vector store? This action cannot be undone.</p>
<div className="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<TremorButton
onClick={onConfirm}
color="red"
className="ml-2"
>
Delete
</TremorButton>
<TremorButton onClick={onCancel} variant="primary">Cancel</TremorButton>
</div>
</Modal>
);
};
export default DeleteModal;

View file

@ -0,0 +1,229 @@
import React, { useState } from "react";
import {
TextInput,
Icon,
Button as TremorButton,
Text,
} from "@tremor/react";
import {
Modal,
Form,
Select,
message,
Tooltip,
Input,
} from "antd";
import { InfoCircleOutlined } from '@ant-design/icons';
import { CredentialItem, vectorStoreCreateCall } from "../networking";
import { Providers, providerLogoMap, provider_map } from "../provider_info_helpers";
interface VectorStoreFormProps {
isVisible: boolean;
onCancel: () => void;
onSuccess: () => void;
accessToken: string | null;
credentials: CredentialItem[];
}
const VectorStoreForm: React.FC<VectorStoreFormProps> = ({
isVisible,
onCancel,
onSuccess,
accessToken,
credentials,
}) => {
const [form] = Form.useForm();
const [metadataJson, setMetadataJson] = useState("{}");
const handleCreate = async (formValues: any) => {
if (!accessToken) return;
try {
// Parse metadata JSON
let metadata = {};
try {
metadata = metadataJson.trim() ? JSON.parse(metadataJson) : {};
} catch (e) {
message.error("Invalid JSON in metadata field");
return;
}
await vectorStoreCreateCall(accessToken, {
vector_store_id: formValues.vector_store_id,
custom_llm_provider: formValues.custom_llm_provider,
vector_store_name: formValues.vector_store_name,
vector_store_description: formValues.vector_store_description,
vector_store_metadata: metadata,
});
message.success("Vector store created successfully");
form.resetFields();
setMetadataJson("{}");
onSuccess();
} catch (error) {
console.error("Error creating vector store:", error);
message.error("Error creating vector store: " + error);
}
};
const handleCancel = () => {
form.resetFields();
setMetadataJson("{}");
onCancel();
};
return (
<Modal
title="Create New Vector Store"
visible={isVisible}
width={800}
footer={null}
onCancel={handleCancel}
>
<Form
form={form}
onFinish={handleCreate}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<Form.Item
label={
<span>
Provider{' '}
<Tooltip title="Select the provider for this vector store">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
</Tooltip>
</span>
}
name="custom_llm_provider"
rules={[{ required: true, message: "Please select a provider" }]}
initialValue="bedrock"
>
<Select>
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => {
// Currently only showing Bedrock since it's the only supported provider
if (providerEnum === 'Bedrock') {
return (
<Select.Option key={providerEnum} value={provider_map[providerEnum]}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {
// Create a div with provider initial as fallback
const target = e.target as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
const fallbackDiv = document.createElement('div');
fallbackDiv.className = 'w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs';
fallbackDiv.textContent = providerDisplayName.charAt(0);
parent.replaceChild(fallbackDiv, target);
}
}}
/>
<span>{providerDisplayName}</span>
</div>
</Select.Option>
);
}
return null;
})}
</Select>
</Form.Item>
<Form.Item
label={
<span>
Vector Store ID{' '}
<Tooltip title="Enter the vector store ID from your api provider">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
</Tooltip>
</span>
}
name="vector_store_id"
rules={[{ required: true, message: "Please input the vector store ID from your api provider" }]}
>
<TextInput />
</Form.Item>
<Form.Item
label={
<span>
Vector Store Name{' '}
<Tooltip title="Custom name you want to give to the vector store, this name will be rendered on the LiteLLM UI">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
</Tooltip>
</span>
}
name="vector_store_name"
>
<TextInput />
</Form.Item>
<Form.Item
label="Description"
name="vector_store_description"
>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item
label={
<span>
Existing Credentials{' '}
<Tooltip title="Optionally select API provider credentials for this vector store eg. Bedrock API KEY">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
</Tooltip>
</span>
}
name="litellm_credential_name"
>
<Select
showSearch
placeholder="Select or search for existing credentials"
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={[
{ value: null, label: 'None' },
...credentials.map((credential) => ({
value: credential.credential_name,
label: credential.credential_name
}))
]}
allowClear
/>
</Form.Item>
<Form.Item
label={
<span>
Metadata{' '}
<Tooltip title="JSON metadata for the vector store (optional)">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
</Tooltip>
</span>
}
>
<Input.TextArea
rows={4}
value={metadataJson}
onChange={(e) => setMetadataJson(e.target.value)}
placeholder='{"key": "value"}'
/>
</Form.Item>
<div className="flex justify-end space-x-3">
<TremorButton onClick={handleCancel} variant="secondary">
Cancel
</TremorButton>
<TremorButton variant="primary" type="submit">
Create
</TremorButton>
</div>
</Form>
</Modal>
);
};
export default VectorStoreForm;

View file

@ -0,0 +1,64 @@
import React, { useEffect, useState } from 'react';
import { Select, Typography, Tooltip } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import { VectorStore } from './types';
import { vectorStoreListCall } from '../networking';
interface VectorStoreSelectorProps {
onChange: (selectedVectorStores: string[]) => void;
value?: string[];
className?: string;
accessToken: string;
}
const VectorStoreSelector: React.FC<VectorStoreSelectorProps> = ({
onChange,
value,
className,
accessToken
}) => {
const [vectorStores, setVectorStores] = useState<VectorStore[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchVectorStores = async () => {
if (!accessToken) return;
setLoading(true);
try {
const response = await vectorStoreListCall(accessToken);
if (response.data) {
setVectorStores(response.data);
}
} catch (error) {
console.error("Error fetching vector stores:", error);
} finally {
setLoading(false);
}
};
fetchVectorStores();
}, [accessToken]);
return (
<div>
<Select
mode="multiple"
placeholder="Select vector stores"
onChange={onChange}
value={value}
loading={loading}
className={className}
options={vectorStores.map(store => ({
label: `${store.vector_store_name || store.vector_store_id} (${store.vector_store_id})`,
value: store.vector_store_id,
title: store.vector_store_description || store.vector_store_id,
}))}
optionFilterProp="label"
showSearch
style={{ width: '100%' }}
/>
</div>
);
};
export default VectorStoreSelector;

View file

@ -0,0 +1,225 @@
import React from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Icon,
Text,
Badge,
} from "@tremor/react";
import {
TrashIcon,
SwitchVerticalIcon,
ChevronUpIcon,
ChevronDownIcon,
} from "@heroicons/react/outline";
import { Tooltip } from "antd";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { VectorStore } from "./types";
import { getProviderLogoAndName } from "../provider_info_helpers";
interface VectorStoreTableProps {
data: VectorStore[];
onDelete: (vectorStoreId: string) => void;
}
const VectorStoreTable: React.FC<VectorStoreTableProps> = ({
data,
onDelete,
}) => {
const [sorting, setSorting] = React.useState<SortingState>([
{ id: "created_at", desc: true }
]);
const columns: ColumnDef<VectorStore>[] = [
{
header: "Vector Store ID",
accessorKey: "vector_store_id",
cell: ({ row }) => {
const vectorStore = row.original;
return (
<div className="overflow-hidden">
<Tooltip title={vectorStore.vector_store_id}>
<span className="font-mono text-blue-500 text-xs font-normal">
{vectorStore.vector_store_id}
</span>
</Tooltip>
</div>
);
},
},
{
header: "Name",
accessorKey: "vector_store_name",
cell: ({ row }) => {
const vectorStore = row.original;
return (
<Tooltip title={vectorStore.vector_store_name}>
<span className="text-xs">
{vectorStore.vector_store_name || "-"}
</span>
</Tooltip>
);
},
},
{
header: "Description",
accessorKey: "vector_store_description",
cell: ({ row }) => {
const vectorStore = row.original;
return (
<Tooltip title={vectorStore.vector_store_description}>
<span className="text-xs">
{vectorStore.vector_store_description || "-"}
</span>
</Tooltip>
);
},
},
{
header: "Provider",
accessorKey: "custom_llm_provider",
cell: ({ row }) => {
const vectorStore = row.original;
const { displayName, logo } = getProviderLogoAndName(vectorStore.custom_llm_provider);
return (
<div className="flex items-center space-x-2">
{logo && <img src={logo} alt={displayName} className="h-4 w-4" />}
<span className="text-xs">{displayName}</span>
</div>
);
},
},
{
header: "Created",
accessorKey: "created_at",
sortingFn: "datetime",
cell: ({ row }) => {
const vectorStore = row.original;
return (
<span className="text-xs">
{new Date(vectorStore.created_at).toLocaleDateString()}
</span>
);
},
},
{
id: "actions",
header: "",
cell: ({ row }) => {
const vectorStore = row.original;
return (
<div className="flex space-x-2">
<Icon
icon={TrashIcon}
size="sm"
onClick={() => onDelete(vectorStore.vector_store_id)}
className="cursor-pointer"
/>
</div>
);
},
},
];
const table = useReactTable({
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
});
return (
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className={`py-1 h-8 ${
header.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]'
: ''
}`}
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
</div>
{header.id !== 'actions' && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
)}
</div>
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]'
: ''
}`}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No vector stores found</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
};
export default VectorStoreTable;

View file

@ -0,0 +1,156 @@
import React, { useState, useEffect } from "react";
import {
Card,
Icon,
Button as TremorButton,
Col,
Text,
Grid,
} from "@tremor/react";
import {
InformationCircleIcon,
RefreshIcon,
} from "@heroicons/react/outline";
import { message } from "antd";
import { vectorStoreListCall, vectorStoreDeleteCall, credentialListCall, CredentialItem } from "../networking";
import { VectorStore } from "./types";
import VectorStoreTable from "./VectorStoreTable";
import VectorStoreForm from "./VectorStoreForm";
import DeleteModal from "./DeleteModal";
interface VectorStoreProps {
accessToken: string | null;
userID: string | null;
userRole: string | null;
}
const VectorStoreManagement: React.FC<VectorStoreProps> = ({
accessToken,
userID,
userRole,
}) => {
const [vectorStores, setVectorStores] = useState<VectorStore[]>([]);
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [vectorStoreToDelete, setVectorStoreToDelete] = useState<string | null>(null);
const [lastRefreshed, setLastRefreshed] = useState("");
const [credentials, setCredentials] = useState<CredentialItem[]>([]);
const fetchVectorStores = async () => {
if (!accessToken) return;
try {
const response = await vectorStoreListCall(accessToken);
console.log("List vector stores response:", response);
setVectorStores(response.data || []);
} catch (error) {
console.error("Error fetching vector stores:", error);
message.error("Error fetching vector stores: " + error);
}
};
const fetchCredentials = async () => {
if (!accessToken) return;
try {
const response = await credentialListCall(accessToken);
console.log("List credentials response:", response);
setCredentials(response.credentials || []);
} catch (error) {
console.error("Error fetching credentials:", error);
message.error("Error fetching credentials: " + error);
}
};
const handleRefreshClick = () => {
fetchVectorStores();
fetchCredentials();
const currentDate = new Date();
setLastRefreshed(currentDate.toLocaleString());
};
const handleDelete = async (vectorStoreId: string) => {
setVectorStoreToDelete(vectorStoreId);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (!accessToken || !vectorStoreToDelete) return;
try {
await vectorStoreDeleteCall(accessToken, vectorStoreToDelete);
message.success("Vector store deleted successfully");
fetchVectorStores();
} catch (error) {
console.error("Error deleting vector store:", error);
message.error("Error deleting vector store: " + error);
}
setIsDeleteModalOpen(false);
setVectorStoreToDelete(null);
};
const handleCreateSuccess = () => {
setIsCreateModalVisible(false);
fetchVectorStores();
};
useEffect(() => {
fetchVectorStores();
fetchCredentials();
}, [accessToken]);
return (
<div className="w-full mx-4 h-[75vh]">
<div className="gap-2 p-8 h-[75vh] w-full mt-2">
<div className="flex justify-between mt-2 w-full items-center mb-4">
<h1>Vector Store Management</h1>
<div className="flex items-center space-x-2">
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
<Icon
icon={RefreshIcon}
variant="shadow"
size="xs"
className="self-center cursor-pointer"
onClick={handleRefreshClick}
/>
</div>
</div>
<Text className="mb-4">
<p>You can use vector stores to store and retrieve LLM embeddings. Currently, we support Amazon Bedrock vector stores.</p>
</Text>
<TremorButton
className="mb-4"
onClick={() => setIsCreateModalVisible(true)}
>
+ Create Vector Store
</TremorButton>
<Grid numItems={1} className="gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
<Col numColSpan={1}>
<VectorStoreTable
data={vectorStores}
onDelete={handleDelete}
/>
</Col>
</Grid>
{/* Create Vector Store Modal */}
<VectorStoreForm
isVisible={isCreateModalVisible}
onCancel={() => setIsCreateModalVisible(false)}
onSuccess={handleCreateSuccess}
accessToken={accessToken}
credentials={credentials}
/>
{/* Delete Confirmation Modal */}
<DeleteModal
isVisible={isDeleteModalOpen}
onCancel={() => setIsDeleteModalOpen(false)}
onConfirm={confirmDelete}
/>
</div>
</div>
);
};
export default VectorStoreManagement;

View file

@ -0,0 +1,43 @@
export interface VectorStore {
vector_store_id: string;
custom_llm_provider: string;
vector_store_name?: string;
vector_store_description?: string;
vector_store_metadata?: Record<string, any>;
created_at: string;
updated_at: string;
created_by?: string;
updated_by?: string;
}
export interface VectorStoreInfoRequest {
vector_store_id: string;
}
export interface VectorStoreNewRequest {
vector_store_id: string;
custom_llm_provider: string;
vector_store_name?: string;
vector_store_description?: string;
vector_store_metadata?: Record<string, any>;
}
export interface VectorStoreUpdateRequest {
vector_store_id: string;
custom_llm_provider?: string;
vector_store_name?: string;
vector_store_description?: string;
vector_store_metadata?: Record<string, any>;
}
export interface VectorStoreDeleteRequest {
vector_store_id: string;
}
export interface VectorStoreListResponse {
object: string;
data: VectorStore[];
total_count: number;
current_page: number;
total_pages: number;
}

View file

@ -0,0 +1,357 @@
import React, { useState, useEffect } from "react";
import {
Card,
Text,
Title,
Button,
Badge,
} from "@tremor/react";
import {
Form,
Input,
Select as Select2,
message,
Tooltip,
Button as AntButton,
} from "antd";
import { InfoCircleOutlined } from '@ant-design/icons';
import { vectorStoreInfoCall, credentialListCall, CredentialItem } from "../networking";
import { VectorStore } from "./types";
import { Providers, providerLogoMap, provider_map } from "../provider_info_helpers";
interface VectorStoreInfoViewProps {
vectorStoreId: string;
onClose: () => void;
accessToken: string | null;
is_admin: boolean;
editVectorStore: boolean;
}
const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
vectorStoreId,
onClose,
accessToken,
is_admin,
editVectorStore,
}) => {
const [form] = Form.useForm();
const [vectorStoreDetails, setVectorStoreDetails] = useState<VectorStore | null>(null);
const [isEditing, setIsEditing] = useState<boolean>(editVectorStore);
const [metadataString, setMetadataString] = useState<string>("{}");
const [credentials, setCredentials] = useState<CredentialItem[]>([]);
const fetchVectorStoreDetails = async () => {
if (!accessToken) return;
try {
const response = await vectorStoreInfoCall(accessToken, vectorStoreId);
if (response && response.vector_store) {
setVectorStoreDetails(response.vector_store);
// If metadata exists and is an object, stringify it for display/editing
if (response.vector_store.vector_store_metadata) {
const metadata = typeof response.vector_store.vector_store_metadata === 'string'
? JSON.parse(response.vector_store.vector_store_metadata)
: response.vector_store.vector_store_metadata;
setMetadataString(JSON.stringify(metadata, null, 2));
}
if (editVectorStore) {
form.setFieldsValue({
vector_store_id: response.vector_store.vector_store_id,
custom_llm_provider: response.vector_store.custom_llm_provider,
vector_store_name: response.vector_store.vector_store_name,
vector_store_description: response.vector_store.vector_store_description,
});
}
}
} catch (error) {
console.error("Error fetching vector store details:", error);
message.error("Error fetching vector store details: " + error);
}
};
const fetchCredentials = async () => {
if (!accessToken) return;
try {
const response = await credentialListCall(accessToken);
console.log("List credentials response:", response);
setCredentials(response.credentials || []);
} catch (error) {
console.error("Error fetching credentials:", error);
}
};
useEffect(() => {
fetchVectorStoreDetails();
fetchCredentials();
}, [vectorStoreId, accessToken]);
const handleSave = async (values: any) => {
if (!accessToken) return;
try {
// Parse the metadata JSON string
let metadata = {};
try {
metadata = metadataString ? JSON.parse(metadataString) : {};
} catch (e) {
message.error("Invalid JSON in metadata field");
return;
}
const updateData = {
vector_store_id: values.vector_store_id,
custom_llm_provider: values.custom_llm_provider,
vector_store_name: values.vector_store_name,
vector_store_description: values.vector_store_description,
vector_store_metadata: metadata,
};
// Use the updated data to call an update endpoint
// await vectorStoreUpdateCall(accessToken, updateData);
message.success("Vector store updated successfully");
setIsEditing(false);
fetchVectorStoreDetails();
} catch (error) {
console.error("Error updating vector store:", error);
message.error("Error updating vector store: " + error);
}
};
if (!vectorStoreDetails) {
return <div>Loading...</div>;
}
return (
<div className="p-4">
<div className="flex justify-between items-center mb-6">
<div>
<Button onClick={onClose} className="mb-4">← Back to Vector Stores</Button>
<Title>Vector Store ID: {vectorStoreDetails.vector_store_id}</Title>
<Text className="text-gray-500">{vectorStoreDetails.vector_store_description || "No description"}</Text>
</div>
{is_admin && !isEditing && (
<Button onClick={() => setIsEditing(true)}>Edit Vector Store</Button>
)}
</div>
{isEditing ? (
<Card>
<Form
form={form}
onFinish={handleSave}
layout="vertical"
initialValues={vectorStoreDetails}
>
<Form.Item
label="Vector Store ID"
name="vector_store_id"
rules={[{ required: true, message: "Please input a vector store ID" }]}
>
<Input disabled />
</Form.Item>
<Form.Item
label="Vector Store Name"
name="vector_store_name"
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name="vector_store_description"
>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item
label={
<span>
Provider{' '}
<Tooltip title="Select the provider for this vector store">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
</Tooltip>
</span>
}
name="custom_llm_provider"
rules={[{ required: true, message: "Please select a provider" }]}
>
<Select2>
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => {
// Currently only showing Bedrock since it's the only supported provider
if (providerEnum === 'Bedrock') {
return (
<Select2.Option key={providerEnum} value={provider_map[providerEnum]}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {
// Create a div with provider initial as fallback
const target = e.target as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
const fallbackDiv = document.createElement('div');
fallbackDiv.className = 'w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs';
fallbackDiv.textContent = providerDisplayName.charAt(0);
parent.replaceChild(fallbackDiv, target);
}
}}
/>
<span>{providerDisplayName}</span>
</div>
</Select2.Option>
);
}
return null;
})}
</Select2>
</Form.Item>
{/* Credentials */}
<div className="mb-4">
<Text className="text-sm text-gray-500 mb-2">
Either select existing credentials OR enter provider credentials below
</Text>
</div>
<Form.Item
label="Existing Credentials"
name="litellm_credential_name"
>
<Select2
showSearch
placeholder="Select or search for existing credentials"
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={[
{ value: null, label: 'None' },
...credentials.map((credential) => ({
value: credential.credential_name,
label: credential.credential_name
}))
]}
allowClear
/>
</Form.Item>
<div className="flex items-center my-4">
<div className="flex-grow border-t border-gray-200"></div>
<span className="px-4 text-gray-500 text-sm">OR</span>
<div className="flex-grow border-t border-gray-200"></div>
</div>
<Form.Item
label={
<span>
Metadata{' '}
<Tooltip title="JSON metadata for the vector store">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
</Tooltip>
</span>
}
>
<Input.TextArea
rows={4}
value={metadataString}
onChange={(e) => setMetadataString(e.target.value)}
placeholder='{"key": "value"}'
/>
</Form.Item>
<div className="flex justify-end space-x-2">
<AntButton onClick={() => setIsEditing(false)}>Cancel</AntButton>
<AntButton type="primary" htmlType="submit">Save Changes</AntButton>
</div>
</Form>
</Card>
) : (
<div className="space-y-6">
<Card>
<Title>Vector Store Details</Title>
<div className="space-y-4 mt-4">
<div>
<Text className="font-medium">ID</Text>
<Text>{vectorStoreDetails.vector_store_id}</Text>
</div>
<div>
<Text className="font-medium">Name</Text>
<Text>{vectorStoreDetails.vector_store_name || "-"}</Text>
</div>
<div>
<Text className="font-medium">Description</Text>
<Text>{vectorStoreDetails.vector_store_description || "-"}</Text>
</div>
<div>
<Text className="font-medium">Provider</Text>
<div className="flex items-center space-x-2 mt-1">
{(() => {
const provider = vectorStoreDetails.custom_llm_provider || "bedrock";
const { displayName, logo } = (() => {
// Find the enum key by matching provider_map values
const enumKey = Object.keys(provider_map).find(
key => provider_map[key].toLowerCase() === provider.toLowerCase()
);
if (!enumKey) {
return { displayName: provider, logo: "" };
}
// Get the display name from Providers enum and logo from map
const displayName = Providers[enumKey as keyof typeof Providers];
const logo = providerLogoMap[displayName];
return { displayName, logo };
})();
return (
<>
{logo && (
<img
src={logo}
alt={`${displayName} logo`}
className="w-5 h-5"
onError={(e) => {
const target = e.target as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
const fallbackDiv = document.createElement('div');
fallbackDiv.className = 'w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs';
fallbackDiv.textContent = displayName.charAt(0);
parent.replaceChild(fallbackDiv, target);
}
}}
/>
)}
<Badge color="blue">{displayName}</Badge>
</>
);
})()}
</div>
</div>
<div>
<Text className="font-medium">Metadata</Text>
<div className="bg-gray-50 p-3 rounded mt-2 font-mono text-xs overflow-auto max-h-48">
<pre>{metadataString}</pre>
</div>
</div>
<div>
<Text className="font-medium">Created</Text>
<Text>{vectorStoreDetails.created_at ? new Date(vectorStoreDetails.created_at).toLocaleString() : "-"}</Text>
</div>
<div>
<Text className="font-medium">Last Updated</Text>
<Text>{vectorStoreDetails.updated_at ? new Date(vectorStoreDetails.updated_at).toLocaleString() : "-"}</Text>
</div>
</div>
</Card>
</div>
)}
</div>
);
};
export default VectorStoreInfoView;