fix(vector-stores): support engines URL for Vertex AI Search (#27885)

Adds optional vertex_engine_id field to vertex_ai/search_api so users
can route through a Discovery Engine search app instead of the data
store directly. Required for website, healthcare, and connector-based
data stores that return FAILED_PRECONDITION on the existing dataStores
URL. Existing data-store-direct callers are unaffected.

Resolves LIT-3036
This commit is contained in:
ryan-crabbe-berri 2026-06-01 16:39:40 -07:00 committed by GitHub
parent 45d41f4104
commit 1cce49b9d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 140 additions and 16 deletions

View File

@ -80,31 +80,47 @@ class VertexSearchAPIVectorStoreConfig(BaseVectorStoreConfig, VertexBase):
litellm_params: dict,
) -> str:
"""
Get the Base endpoint for Vertex AI Search API
Get the Base endpoint for Vertex AI Search API.
Branches on whether a `vertex_engine_id` is configured:
- Engine ID present: route through the search app (engine) required for website,
healthcare, and connector-based data stores. Note the serving config name differs
(`default_serving_config` vs `default_config` for direct data store search).
- Engine ID absent: query the data store directly via `vector_store_id`.
"""
if api_base:
return api_base.rstrip("/")
vertex_location = self.get_vertex_ai_location(litellm_params)
vertex_project = self.get_vertex_ai_project(litellm_params)
collection_id = (
litellm_params.get("vertex_collection_id") or "default_collection"
)
datastore_id = litellm_params.get("vector_store_id")
if not datastore_id:
raise ValueError("vector_store_id is required")
if api_base:
return api_base.rstrip("/")
encoded_collection_id = encode_url_path_segment(
collection_id, field_name="vertex_collection_id"
)
base = (
f"https://discoveryengine.googleapis.com/v1/"
f"projects/{vertex_project}/locations/{vertex_location}/"
f"collections/{encoded_collection_id}"
)
engine_id = litellm_params.get("vertex_engine_id")
if engine_id:
encoded_engine_id = encode_url_path_segment(
engine_id, field_name="vertex_engine_id"
)
return f"{base}/engines/{encoded_engine_id}/servingConfigs/default_serving_config"
datastore_id = litellm_params.get("vector_store_id")
if not datastore_id:
raise ValueError(
"vector_store_id is required when vertex_engine_id is not set"
)
encoded_datastore_id = encode_url_path_segment(
datastore_id, field_name="vector_store_id"
)
# Vertex AI Search API endpoint for search
return (
f"https://discoveryengine.googleapis.com/v1/"
f"projects/{vertex_project}/locations/{vertex_location}/"
f"collections/{encoded_collection_id}/dataStores/{encoded_datastore_id}/servingConfigs/default_config"
)
return f"{base}/dataStores/{encoded_datastore_id}/servingConfigs/default_config"
def transform_search_vector_store_request(
self,

View File

@ -38,3 +38,91 @@ def test_should_reject_dot_segment_vertex_search_vector_store_id():
"vector_store_id": "..",
},
)
def test_should_use_engines_url_when_engine_id_provided():
config = VertexSearchAPIVectorStoreConfig()
url = config.get_complete_url(
api_base=None,
litellm_params={
"vertex_project": "test-project",
"vertex_location": "global",
"vertex_engine_id": "test-engine_1234",
},
)
assert url == (
"https://discoveryengine.googleapis.com/v1/"
"projects/test-project/locations/global/"
"collections/default_collection/engines/test-engine_1234/servingConfigs/default_serving_config"
)
def test_engine_id_takes_precedence_over_vector_store_id():
config = VertexSearchAPIVectorStoreConfig()
url = config.get_complete_url(
api_base=None,
litellm_params={
"vertex_project": "test-project",
"vertex_location": "global",
"vertex_engine_id": "test-engine_1234",
"vector_store_id": "ignored-when-engine-set",
},
)
assert "/engines/test-engine_1234/" in url
assert "/dataStores/" not in url
assert url.endswith("/servingConfigs/default_serving_config")
def test_should_encode_vertex_engine_id_in_complete_url():
config = VertexSearchAPIVectorStoreConfig()
url = config.get_complete_url(
api_base=None,
litellm_params={
"vertex_project": "test-project",
"vertex_location": "global",
"vertex_engine_id": "../../engines/other?x=1#frag",
},
)
assert url == (
"https://discoveryengine.googleapis.com/v1/"
"projects/test-project/locations/global/"
"collections/default_collection/engines/..%2F..%2Fengines%2Fother%3Fx%3D1%23frag/servingConfigs/default_serving_config"
)
def test_should_reject_dot_segment_vertex_engine_id():
config = VertexSearchAPIVectorStoreConfig()
with pytest.raises(
ValueError, match="vertex_engine_id cannot be a dot path segment"
):
config.get_complete_url(
api_base=None,
litellm_params={
"vertex_project": "test-project",
"vertex_location": "global",
"vertex_engine_id": "..",
},
)
def test_should_raise_when_neither_engine_id_nor_vector_store_id_provided():
config = VertexSearchAPIVectorStoreConfig()
with pytest.raises(
ValueError,
match="vector_store_id is required when vertex_engine_id is not set",
):
config.get_complete_url(
api_base=None,
litellm_params={
"vertex_project": "test-project",
"vertex_location": "global",
},
)

View File

@ -32,6 +32,7 @@ const VectorStoreForm: React.FC<VectorStoreFormProps> = ({
const [metadataJson, setMetadataJson] = useState("{}");
const [selectedProvider, setSelectedProvider] = useState("bedrock");
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
const vertexEngineId = Form.useWatch("vertex_engine_id", form);
useEffect(() => {
if (!accessToken) return;
@ -230,8 +231,16 @@ const VectorStoreForm: React.FC<VectorStoreFormProps> = ({
</a>
</li>
<li>Pick a supported location: global, us, or eu</li>
<li>Copy the data store ID from the Vertex AI Search console</li>
<li>Enter the data store ID in the Vector Store ID field below</li>
<li>
For most data store types (Cloud Storage, BigQuery, Media): copy the data store ID and enter it in
the Vector Store ID field below.
</li>
<li>
For website, healthcare, and connector-based sources (Drive, Gmail, Slack, Jira, etc.): create a
search app on top of the data store, then copy the <strong>Engine ID</strong> and enter it in the
Engine ID field. The Vector Store ID is still required as the LiteLLM-side name for this record,
but it isn't used in the GCP URL when Engine ID is set.
</li>
</ol>
</div>
}
@ -258,7 +267,9 @@ const VectorStoreForm: React.FC<VectorStoreFormProps> = ({
selectedProvider === "vertex_rag_engine"
? "6917529027641081856 (Get corpus ID from Vertex AI console)"
: selectedProvider === "vertex_ai/search_api"
? "my-datastore_1234567890 (Get data store ID from Vertex AI Search console)"
? vertexEngineId
? "Any identifier you'll use to reference this in LiteLLM"
: "my-datastore_1234567890 (Get data store ID from Vertex AI Search console)"
: "Enter vector store ID from your provider"
}
/>

View File

@ -97,6 +97,15 @@ export const vectorStoreProviderFields: Record<string, VectorStoreFieldConfig[]>
required: false,
type: "text",
},
{
name: "vertex_engine_id",
label: "Engine ID (optional)",
tooltip:
"Search app (engine) ID. Required for website, healthcare, and connector-based data stores (Workspace, Slack, Jira, etc.) because these sources route search through an engine. Leave blank to query the data store directly.",
placeholder: "e.g. my-search-app_1234567890",
required: false,
type: "text",
},
],
openai: [
{