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:
parent
45d41f4104
commit
1cce49b9d0
@ -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,
|
||||
|
||||
@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
@ -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"
|
||||
}
|
||||
/>
|
||||
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user