[Feat] LLM API Endpoint - Expose OpenAI Compatible /vector_stores/{vector_store_id}/search endpoint (#12749)

* fix _pass_through_endpoint_without_required_model

* add get_litellm_managed_vector_store_from_registry

* undo router change

* fix for using router + vector search methods

* add simple helper for _update_request_data_with_litellm_managed_vector_store_registry

* add vector_stores routes

* test_router_avector_store_search_passes_correct_args

* [Feat] UI - Allow clicking into Vector Stores (#12741)

* Add View Vector Store

* add /info for vector store

* fix updated_at

* allow easily testing the KB on litellm

* fix

* rename test

* test_init_vector_store_api_endpoints

* test_update_request_data_with_litellm_managed_vector_store_registry
This commit is contained in:
Ishaan Jaff
2025-07-18 18:18:53 -07:00
committed by GitHub
parent 8d35a00974
commit 5802a5bbe3
10 changed files with 805 additions and 218 deletions
+6
View File
@@ -307,6 +307,12 @@ class LiteLLMRoutes(enum.Enum):
"/v1/responses/{response_id}",
"/responses/{response_id}/input_items",
"/v1/responses/{response_id}/input_items",
# vector stores
"/vector_stores",
"/v1/vector_stores",
"/vector_stores/{vector_store_id}/search",
"/v1/vector_stores/{vector_store_id}/search",
]
mapped_pass_through_routes = [
@@ -1,5 +1,11 @@
from typing import Dict, Optional
from fastapi import APIRouter, Depends, Request, Response
import litellm
from litellm.integrations.vector_store_integrations.vector_store_pre_call_hook import (
LiteLLM_ManagedVectorStore,
)
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
@@ -9,6 +15,30 @@ router = APIRouter()
# OpenAI Compatible Endpoints
########################################################
def _update_request_data_with_litellm_managed_vector_store_registry(
data: Dict,
vector_store_id: str,
) -> Dict:
"""
Update the request data with the litellm managed vector store registry.
"""
if litellm.vector_store_registry is not None:
vector_store_to_run: Optional[LiteLLM_ManagedVectorStore] = litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry(
vector_store_id=vector_store_id
)
if vector_store_to_run is not None:
if "custom_llm_provider" in vector_store_to_run:
data["custom_llm_provider"] = vector_store_to_run.get("custom_llm_provider")
if "litellm_credential_name" in vector_store_to_run:
data["litellm_credential_name"] = vector_store_to_run.get("litellm_credential_name")
if "litellm_params" in vector_store_to_run:
litellm_params = vector_store_to_run.get("litellm_params", {}) or {}
data.update(litellm_params)
return data
@router.post("/v1/vector_stores/{vector_store_id}/search", dependencies=[Depends(user_api_key_auth)])
@router.post("/vector_stores/{vector_store_id}/search", dependencies=[Depends(user_api_key_auth)])
async def vector_store_search(
@@ -41,6 +71,12 @@ async def vector_store_search(
data = await _read_request_body(request=request)
if "vector_store_id" not in data:
data["vector_store_id"] = vector_store_id
data = _update_request_data_with_litellm_managed_vector_store_registry(
data=data,
vector_store_id=vector_store_id
)
processor = ProxyBaseLLMRequestProcessing(data=data)
try:
return await processor.base_process_llm_request(
+26 -5
View File
@@ -3262,6 +3262,7 @@ class Router:
async def _pass_through_moderation_endpoint_factory(
self,
original_function: Callable,
custom_llm_provider: Optional[str] = None,
**kwargs,
):
# update kwargs with model_group
@@ -3328,7 +3329,7 @@ class Router:
def sync_wrapper(
custom_llm_provider: Optional[
Literal["openai", "azure", "anthropic"]
str
] = None,
client: Optional[Any] = None,
**kwargs,
@@ -3342,7 +3343,7 @@ class Router:
# Handle asynchronous call types
async def async_wrapper(
custom_llm_provider: Optional[
Literal["openai", "azure", "anthropic"]
str
] = None,
client: Optional[Any] = None,
**kwargs,
@@ -3370,8 +3371,6 @@ class Router:
"aimage_edit",
"agenerate_content",
"agenerate_content_stream",
"avector_store_search",
"avector_store_create",
):
return await self._ageneric_api_call_with_fallbacks(
original_function=original_function,
@@ -3392,6 +3391,15 @@ class Router:
original_function=original_function,
**kwargs,
)
elif call_type in (
"avector_store_search",
"avector_store_create",
):
return await self._init_vector_store_api_endpoints(
original_function=original_function,
custom_llm_provider=custom_llm_provider,
**kwargs,
)
elif call_type in ("afile_delete", "afile_content"):
return await self._ageneric_api_call_with_fallbacks(
original_function=original_function,
@@ -3401,6 +3409,19 @@ class Router:
)
return async_wrapper
async def _init_vector_store_api_endpoints(
self,
original_function: Callable,
custom_llm_provider: Optional[str] = None,
**kwargs,
):
"""
Initialize the Vector Store API endpoints on the router.
"""
if custom_llm_provider and "custom_llm_provider" not in kwargs:
kwargs["custom_llm_provider"] = custom_llm_provider
return await original_function(**kwargs)
async def _init_responses_api_endpoints(
self,
@@ -3427,7 +3448,7 @@ class Router:
async def _pass_through_assistants_endpoint_factory(
self,
original_function: Callable,
custom_llm_provider: Optional[Literal["openai", "azure", "anthropic"]] = None,
custom_llm_provider: Optional[str] = None,
client: Optional[AsyncOpenAI] = None,
**kwargs,
):
@@ -128,6 +128,17 @@ class VectorStoreRegistry:
return vector_store
return None
def get_litellm_managed_vector_store_from_registry(
self, vector_store_id: str
) -> Optional[LiteLLM_ManagedVectorStore]:
"""
Returns the vector store from the registry
"""
for vector_store in self.vector_stores:
if vector_store.get("vector_store_id") == vector_store_id:
return vector_store
return None
def pop_vector_stores_to_run(
self, non_default_params: Dict, tools: Optional[List[Dict]] = None
) -> List[LiteLLM_ManagedVectorStore]:
@@ -617,3 +617,51 @@ async def test_init_responses_api_endpoints():
assert second_call_kwargs["model"] == "claude-3-sonnet"
assert second_call_kwargs["response_id"] == "resp_claude_123"
@pytest.mark.asyncio
async def test_init_vector_store_api_endpoints():
"""
Test that _init_vector_store_api_endpoints correctly passes custom_llm_provider to kwargs
"""
# Create a router with a basic model
router = Router(
model_list=[
{
"model_name": "test-model",
"litellm_params": {
"model": "openai/test-model",
"api_key": "fake-api-key",
},
}
]
)
# Mock the original function
mock_original_function = AsyncMock(return_value={"status": "success"})
# Call without custom_llm_provider
result = await router._init_vector_store_api_endpoints(
original_function=mock_original_function,
vector_store_id="test-store"
)
# Verify original function was called with correct kwargs
mock_original_function.assert_called_once_with(vector_store_id="test-store")
assert result == {"status": "success"}
# Reset the mock
mock_original_function.reset_mock()
# Call with custom_llm_provider
await router._init_vector_store_api_endpoints(
original_function=mock_original_function,
custom_llm_provider="openai",
vector_store_id="test-store"
)
# Verify custom_llm_provider was added to kwargs
mock_original_function.assert_called_once_with(
vector_store_id="test-store",
custom_llm_provider="openai"
)
@@ -0,0 +1,102 @@
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
sys.path.insert(
0, os.path.abspath("../../../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.integrations.vector_store_integrations.vector_store_pre_call_hook import (
LiteLLM_ManagedVectorStore,
)
from litellm.proxy.vector_store_endpoints.endpoints import (
_update_request_data_with_litellm_managed_vector_store_registry,
)
@pytest.mark.asyncio
async def test_router_avector_store_search_passes_correct_args():
"""
Test that router.avector_store_search() passes the correct arguments
to downstream litellm.vector_stores.asearch() with custom_llm_provider and query.
"""
# Create a router
router = litellm.Router(model_list=[])
# Mock the router's _init_vector_store_api_endpoints method to avoid real API calls
with patch.object(router, '_init_vector_store_api_endpoints') as mock_init:
mock_init.return_value = {
"object": "vector_store.search_results.page",
"search_query": "test query",
"data": []
}
# Call router's avector_store_search
result = await router.avector_store_search(
vector_store_id="test_store_id",
query="test query",
custom_llm_provider="bedrock"
)
# Verify the internal method was called with correct args
mock_init.assert_called_once()
call_args = mock_init.call_args
# Check that the original function is passed correctly
assert call_args[1]["vector_store_id"] == "test_store_id"
assert call_args[1]["query"] == "test query"
assert call_args[1]["custom_llm_provider"] == "bedrock"
def test_update_request_data_with_litellm_managed_vector_store_registry():
"""
Test that _update_request_data_with_litellm_managed_vector_store_registry
correctly updates request data with vector store registry information.
"""
# Setup test data
data = {"existing_key": "existing_value"}
vector_store_id = "test_store_id"
# Mock vector store registry
mock_vector_store: LiteLLM_ManagedVectorStore = {
"vector_store_id": "test_store_id",
"custom_llm_provider": "bedrock",
"litellm_credential_name": "test_credential",
"litellm_params": {"api_key": "test_key", "aws_region_name": "us-east-1"}
}
mock_registry = MagicMock()
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = mock_vector_store
# Test with vector store registry
with patch.object(litellm, 'vector_store_registry', mock_registry):
result = _update_request_data_with_litellm_managed_vector_store_registry(
data=data,
vector_store_id=vector_store_id
)
# Verify the data was updated correctly
assert result["existing_key"] == "existing_value" # Original data preserved
assert result["custom_llm_provider"] == "bedrock"
assert result["litellm_credential_name"] == "test_credential"
assert result["api_key"] == "test_key"
assert result["aws_region_name"] == "us-east-1"
# Verify registry was called correctly
mock_registry.get_litellm_managed_vector_store_from_registry.assert_called_once_with(
vector_store_id="test_store_id"
)
# Test with no vector store registry
with patch.object(litellm, 'vector_store_registry', None):
original_data = {"existing_key": "existing_value"}
result = _update_request_data_with_litellm_managed_vector_store_registry(
data=original_data,
vector_store_id=vector_store_id
)
# Verify data remains unchanged when no registry
assert result == original_data
@@ -98,4 +98,17 @@ class TestBedrockVectorStore(BaseVectorStoreTest):
# Test with None
attributes = config._get_attributes_from_metadata(None)
assert attributes == {}
assert attributes == {}
@pytest.mark.asyncio
async def test_bedrock_search_with_router():
from litellm.router import Router
# init router
_router = Router(model_list=[])
search_response = await _router.avector_store_search(
query="what happens after we add a model",
vector_store_id="T37J8R4WTM",
custom_llm_provider="bedrock",
)
print(search_response)
@@ -6226,3 +6226,35 @@ export const testMCPToolsListRequest = async (
throw error;
}
};
export const vectorStoreSearchCall = async (
accessToken: string,
vectorStoreId: string,
query: string
): Promise<any> => {
try {
const url = `${getProxyBaseUrl()}/v1/vector_stores/${vectorStoreId}/search`;
const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: query
}),
});
if (!response.ok) {
const errorData = await response.text();
await handleError(errorData);
return null;
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error testing vector store search:", error);
throw error;
}
};
@@ -0,0 +1,280 @@
import React, { useState } from "react";
import { Button, Input, Card, Typography, Spin, message, Divider } from "antd";
import { SendOutlined, DatabaseOutlined, LoadingOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
import { vectorStoreSearchCall } from "../networking";
const { TextArea } = Input;
const { Text, Title } = Typography;
interface VectorStoreContent {
text: string;
type: string;
}
interface VectorStoreResult {
score: number;
content: VectorStoreContent[];
file_id?: string;
filename?: string;
attributes?: Record<string, any>;
}
interface VectorStoreSearchResponse {
object: string;
search_query: string;
data: VectorStoreResult[];
}
interface VectorStoreTesterProps {
vectorStoreId: string;
accessToken: string;
className?: string;
}
export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({
vectorStoreId,
accessToken,
className = "",
}) => {
const [query, setQuery] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [searchHistory, setSearchHistory] = useState<{
query: string;
response: VectorStoreSearchResponse | null;
timestamp: number;
}[]>([]);
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
const handleSearch = async () => {
if (!query.trim()) {
message.warning("Please enter a search query");
return;
}
setIsLoading(true);
try {
const response = await vectorStoreSearchCall(accessToken, vectorStoreId, query);
const historyEntry = {
query,
response,
timestamp: Date.now(),
};
setSearchHistory(prev => [historyEntry, ...prev]);
setQuery("");
} catch (error) {
console.error("Error searching vector store:", error);
message.error("Failed to search vector store");
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSearch();
}
};
const formatTimestamp = (timestamp: number): string => {
return new Date(timestamp).toLocaleString();
};
const clearHistory = () => {
setSearchHistory([]);
setExpandedResults({});
message.success("Search history cleared");
};
const toggleResultExpansion = (historyIndex: number, resultIndex: number) => {
const key = `${historyIndex}-${resultIndex}`;
setExpandedResults(prev => ({
...prev,
[key]: !prev[key]
}));
};
return (
<Card className="w-full rounded-xl shadow-md">
<div className="flex flex-col h-[600px]">
{/* Header */}
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<div className="flex items-center">
<DatabaseOutlined className="mr-2 text-blue-500" />
<Title level={4} className="mb-0">Test Vector Store</Title>
</div>
{searchHistory.length > 0 && (
<Button onClick={clearHistory} size="small">
Clear History
</Button>
)}
</div>
{/* Results Area */}
<div className="flex-1 overflow-auto p-4 pb-0">
{searchHistory.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-gray-400">
<DatabaseOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
<Text>Test your vector store by entering a search query below</Text>
</div>
) : (
<div className="space-y-4">
{searchHistory.map((entry, index) => (
<div key={index} className="space-y-2">
{/* User Query */}
<div className="text-right">
<div className="inline-block max-w-[80%] rounded-lg shadow-sm p-3 bg-blue-50 border border-blue-200">
<div className="flex items-center gap-2 mb-1">
<strong className="text-sm">Query</strong>
<span className="text-xs text-gray-500">
{formatTimestamp(entry.timestamp)}
</span>
</div>
<div className="text-left">
{entry.query}
</div>
</div>
</div>
{/* Vector Store Response */}
<div className="text-left">
<div className="inline-block max-w-[80%] rounded-lg shadow-sm p-3 bg-white border border-gray-200">
<div className="flex items-center gap-2 mb-2">
<DatabaseOutlined className="text-green-500" />
<strong className="text-sm">Vector Store Results</strong>
{entry.response && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-100 text-gray-600">
{entry.response.data?.length || 0} results
</span>
)}
</div>
{entry.response && entry.response.data && entry.response.data.length > 0 ? (
<div className="space-y-3">
{entry.response.data.map((result, resultIndex) => {
const isExpanded = expandedResults[`${index}-${resultIndex}`] || false;
return (
<div key={resultIndex} className="border rounded-lg overflow-hidden bg-gray-50">
{/* Clickable Header */}
<div
className="flex justify-between items-center p-3 cursor-pointer hover:bg-gray-100 transition-colors"
onClick={() => toggleResultExpansion(index, resultIndex)}
>
<div className="flex items-center">
{isExpanded ? (
<DownOutlined className="text-gray-500 mr-2" />
) : (
<RightOutlined className="text-gray-500 mr-2" />
)}
<span className="font-medium text-sm">Result {resultIndex + 1}</span>
{/* Show preview of content when collapsed */}
{!isExpanded && result.content && result.content[0] && (
<span className="ml-2 text-xs text-gray-500 truncate max-w-md">
- {result.content[0].text.substring(0, 100)}...
</span>
)}
</div>
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
Score: {result.score.toFixed(4)}
</span>
</div>
{/* Expandable Content */}
{isExpanded && (
<div className="border-t bg-white p-3">
{/* Content */}
{result.content && result.content.map((content, contentIndex) => (
<div key={contentIndex} className="mb-3">
<div className="text-xs text-gray-500 mb-1">Content ({content.type})</div>
<div className="text-sm bg-gray-50 p-3 rounded border text-gray-800 max-h-40 overflow-y-auto">
{content.text}
</div>
</div>
))}
{/* Metadata */}
{(result.file_id || result.filename || result.attributes) && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="text-xs text-gray-500 mb-2 font-medium">Metadata</div>
<div className="space-y-2 text-xs">
{result.file_id && (
<div className="bg-gray-50 p-2 rounded">
<span className="font-medium">File ID:</span> {result.file_id}
</div>
)}
{result.filename && (
<div className="bg-gray-50 p-2 rounded">
<span className="font-medium">Filename:</span> {result.filename}
</div>
)}
{result.attributes && Object.keys(result.attributes).length > 0 && (
<div className="bg-gray-50 p-2 rounded">
<span className="font-medium block mb-1">Attributes:</span>
<pre className="text-xs bg-white p-2 rounded border overflow-x-auto">
{JSON.stringify(result.attributes, null, 2)}
</pre>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
);
})}
</div>
) : (
<div className="text-gray-500 text-sm">No results found</div>
)}
</div>
</div>
{index < searchHistory.length - 1 && <Divider />}
</div>
))}
</div>
)}
{isLoading && (
<div className="flex justify-center items-center my-4">
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
</div>
)}
</div>
{/* Input Area */}
<div className="p-4 border-t border-gray-200 bg-white">
<div className="flex items-end space-x-2">
<div className="flex-1">
<TextArea
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter your search query... (Shift+Enter for new line)"
disabled={isLoading}
autoSize={{ minRows: 1, maxRows: 4 }}
style={{ resize: 'none' }}
/>
</div>
<Button
type="primary"
onClick={handleSearch}
disabled={isLoading || !query.trim()}
icon={<SendOutlined />}
loading={isLoading}
>
Search
</Button>
</div>
</div>
</div>
</Card>
);
};
export default VectorStoreTester;
@@ -5,6 +5,11 @@ import {
Title,
Button,
Badge,
TabGroup,
TabList,
Tab,
TabPanels,
TabPanel,
} from "@tremor/react";
import {
Form,
@@ -19,6 +24,7 @@ import { ArrowLeftIcon } from "@heroicons/react/outline";
import { vectorStoreInfoCall, vectorStoreUpdateCall, credentialListCall, CredentialItem } from "../networking";
import { VectorStore } from "./types";
import { Providers, providerLogoMap, provider_map } from "../provider_info_helpers";
import VectorStoreTester from "./VectorStoreTester";
interface VectorStoreInfoViewProps {
vectorStoreId: string;
@@ -40,6 +46,7 @@ const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
const [isEditing, setIsEditing] = useState<boolean>(editVectorStore);
const [metadataString, setMetadataString] = useState<string>("{}");
const [credentials, setCredentials] = useState<CredentialItem[]>([]);
const [activeTab, setActiveTab] = useState<string>(editVectorStore ? "details" : "details");
const fetchVectorStoreDetails = async () => {
if (!accessToken) return;
@@ -134,224 +141,255 @@ const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
{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">
<TabGroup>
<TabList className="mb-6">
<Tab>Details</Tab>
<Tab>Test Vector Store</Tab>
</TabList>
<TabPanels>
{/* Details Tab */}
<TabPanel>
{isEditing ? (
<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: "" };
<div className="flex justify-between items-center mb-4">
<Title>Edit Vector Store</Title>
</div>
<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>
}
// 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>
</>
);
})()}
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>
) : (
<div>
<div className="flex justify-between items-center mb-4">
<Title>Vector Store Details</Title>
{is_admin && (
<Button onClick={() => setIsEditing(true)}>Edit Vector Store</Button>
)}
</div>
<Card>
<div className="space-y-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>
<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>
)}
)}
</TabPanel>
{/* Test Tab */}
<TabPanel>
<VectorStoreTester
vectorStoreId={vectorStoreDetails.vector_store_id}
accessToken={accessToken || ""}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
);
};