Skip to content

feat(rag): add Milvus vector store ingestion support - #30388

Merged
Sameerlite merged 16 commits into
BerriAI:litellm_oss_staging_230626from
maycuatroi1:feat/milvus-vector-store-ingest
Jun 23, 2026
Merged

feat(rag): add Milvus vector store ingestion support#30388
Sameerlite merged 16 commits into
BerriAI:litellm_oss_staging_230626from
maycuatroi1:feat/milvus-vector-store-ingest

Conversation

@maycuatroi1

@maycuatroi1 maycuatroi1 commented Jun 13, 2026

Copy link
Copy Markdown

Relevant issues

Adds write/ingest support for Milvus to the /rag/ingest pipeline. Milvus is currently supported only for search/retrieval (litellm/llms/milvus/vector_stores); this PR brings it to parity by letting users ingest documents into a self-hosted Milvus through custom_llm_provider="milvus".

Related: #26771. That issue reports the same underlying limitation from the other end - a self-hosted vector store (pg_vector) failing ingest with Provider 'X' is not supported for RAG ingestion. Supported providers: openai, bedrock, gemini, s3_vectors, vertex_ai, raised from the exact same code path (litellm/rag/main.py -> get_ingestion_class -> INGESTION_REGISTRY). I hit the same wall while wiring up a self-hosted Milvus for RAG ingest, and this PR fixes it for Milvus by extending that registry.

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests
  • My PR's scope is as isolated as possible; it only solves 1 specific problem

Type

🆕 New Feature

Changes

  • New litellm/rag/ingestion/milvus_ingestion.py (MilvusRAGIngestion) implementing the store() step on top of the existing BaseRAGIngestion upload/OCR/chunk/embed pipeline.
    • Talks to Milvus over the REST API v2 (/v2/vectordb/entities/insert) via httpx - no pymilvus dependency.
    • Auto-creates the collection with the Milvus "quick setup" API (dynamic fields enabled) when it doesn't exist, using the embedding dimension detected from the first embedding.
    • Embeddings generated through the standard litellm embedding API (any provider).
    • api_key is optional (self-hosted Milvus without auth); supports db_name and partition_name.
  • Registered "milvus" in INGESTION_REGISTRY (litellm/rag/main.py).
  • Added MilvusVectorStoreOptions TypedDict and included it in RAGIngestVectorStoreOptions (litellm/types/rag.py).
  • Added MILVUS_DEFAULT_VECTOR_FIELD / MILVUS_DEFAULT_TEXT_FIELD / MILVUS_DEFAULT_METRIC_TYPE constants.

Usage

litellm.rag.ingest(
    ingest_options={
        "embedding": {"model": "text-embedding-3-small"},
        "chunking_strategy": {"chunk_size": 256, "chunk_overlap": 50},
        "vector_store": {
            "custom_llm_provider": "milvus",
            "collection_name": "my_docs",
            "api_base": "http://localhost:19530",  # or MILVUS_API_BASE env / server-side config
        },
    },
    file_data=("doc.txt", b"...", "text/plain"),
)

Tests

  • tests/test_litellm/rag/test_milvus_ingestion.py - 16 unit tests (mocked Milvus REST): config/defaults, optional auth header, auto-create vs existing collection, auto_create_collection=False, insert body shape (vector/text/metadata), error propagation on non-zero Milvus code, db/partition propagation, embedding via litellm vs router, and the collection-exists error fallback.
  • tests/vector_store_tests/rag/test_rag_milvus.py - env-gated integration test (skipped unless MILVUS_API_BASE set), follows the existing BaseRAGTest pattern.
$ python -m pytest tests/test_litellm/rag/test_milvus_ingestion.py -q
16 passed

Working proof - live /rag/ingest against milvusdb/milvus:v2.5.4

curl -X POST $PROXY/rag/ingest -H "Authorization: Bearer $KEY" \
  -F "file=@devproof.txt;type=text/plain" \
  -F 'request={"ingest_options":{"embedding":{"model":"text-embedding-3-small"},
       "vector_store":{"custom_llm_provider":"milvus","collection_name":"pr30388_dev_proof"}}}'
# {"status":"completed","vector_store_id":"pr30388_dev_proof","file_id":"devproof.txt"}

Auto-created collection is queryable (POST /v2/vectordb/entities/query):

{"code":0,"data":[{"chunk_index":0,"filename":"devproof.txt","text":"LiteLLM PR 30388 proof. Photosynthesis ..."}]}

Shows up in the dashboard as:

image image
POST /rag/ingest  {custom_llm_provider: milvus, collection_name: evo_probe, embedding: text-embedding-3-small}
-> {"id":"ingest_8b285dfe-...","status":"completed","vector_store_id":"evo_probe","file_id":"evo_probe.txt"}

# querying the collection afterwards:
/v2/vectordb/collections/has  -> {"code":0,"data":{"has":true}}    # auto-created
/v2/vectordb/entities/query   -> row {chunk_index:0, filename:"evo_probe.txt", text:"EVO LMS knowledge base. Photosynthesis...", vector:[...]}

@CLAassistant

CLAassistant commented Jun 13, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@maycuatroi1

Copy link
Copy Markdown
Author

@greptileai

@codspeed-hq

codspeed-hq Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Congrats! CodSpeed is installed 🎉

🆕 16 new benchmarks were detected.

You will start to see performance impacts in the reports once the benchmarks are run from your default branch.

Detected benchmarks


Open in CodSpeed

Comment thread ui/litellm-dashboard/src/app/(dashboard)/page.tsx Fixed
@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.68874% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/rag_endpoints/endpoints.py 86.11% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@maycuatroi1
maycuatroi1 force-pushed the feat/milvus-vector-store-ingest branch from a1ca56b to 569542b Compare June 13, 2026 17:37

from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple

import litellm
Comment thread litellm/rag/ingestion/milvus_ingestion.py
@veria-ai

veria-ai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 7 · PR risk: 0/10

@maycuatroi1
maycuatroi1 force-pushed the feat/milvus-vector-store-ingest branch from 569542b to 67059f3 Compare June 13, 2026 17:49
@maycuatroi1
maycuatroi1 changed the base branch from main to litellm_oss_branch June 13, 2026 17:49
@maycuatroi1
maycuatroi1 requested a review from a team June 13, 2026 17:49
@maycuatroi1
maycuatroi1 force-pushed the feat/milvus-vector-store-ingest branch from 67059f3 to c27d0de Compare June 13, 2026 17:50
@Sameerlite

Copy link
Copy Markdown
Contributor

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds Milvus as a supported RAG ingestion provider by registering MilvusRAGIngestion in the ingestion registry and wiring up the full upload → chunk → embed → insert pipeline over the Milvus REST API v2 (no pymilvus dependency). Authorization and credential-hydration security are addressed through a new polymorphic dispatch pattern (normalize_authorized_vector_store_id, credential_protected_fields, can_auto_create_vector_store) that keeps provider-specific logic in the provider class rather than the proxy router.

  • New MilvusRAGIngestion: uses litellm's httpx infrastructure, auto-creates collections via the Milvus quick-setup API, scopes the env api_key to the env api_base to prevent credential leakage to user-supplied endpoints, and reads db_name/partition_name from env-only to prevent request-controlled write-target shifts.
  • Authorization refactor in endpoints.py: _normalize_collection_name_as_vector_store_id ensures collection_name is always authorized as the write target; the VIEW_ONLY managed-store check now fires only for providers that can auto-create (Milvus), preserving backward compatibility for OpenAI, Bedrock, and other pre-existing provider-native IDs.
  • BaseRAGIngestion extension: three new classmethods with safe defaults allow future providers to opt in to the same authorization patterns without changes to the proxy router.

Confidence Score: 5/5

Safe to merge; the new Milvus ingestion path is well-isolated, uses litellm's existing HTTP infrastructure, and the authorization refactor is backward-compatible for all existing providers.

The security-sensitive changes (authorization normalization, credential-hydration protection, VIEW_ONLY role guard) are all covered by targeted tests, and the logic correctly short-circuits the managed-store check for non-auto-create providers so existing OpenAI/Bedrock users are unaffected. The only observation is a minor debugging ergonomics issue in _collection_exists.

No files require special attention; litellm/rag/ingestion/milvus_ingestion.py is the most novel file and the one comment there is non-blocking.

Important Files Changed

Filename Overview
litellm/rag/ingestion/milvus_ingestion.py New Milvus ingestion class: correctly uses litellm's httpx infrastructure, credential-protected fields prevent auth bypass, api_key env fallback is scoped to env-supplied api_base, defaults wired in __init__. Minor: _collection_exists swallows all exceptions.
litellm/proxy/rag_endpoints/endpoints.py Authorization flow refactored: provider-specific write-target normalization is dispatched polymorphically via get_ingestion_class, VIEW_ONLY managed-store check is now conditional on provider auto-create capability (backward-compatible for OpenAI/Bedrock), addresses all previously flagged bypass vectors.
litellm/rag/ingestion/base_ingestion.py Adds normalize_authorized_vector_store_id, credential_protected_fields, and can_auto_create_vector_store classmethods with safe defaults; credential hydration now skips protected fields, preventing post-authorization write-target override.
litellm/types/rag.py Adds MilvusVectorStoreOptions TypedDict and includes it in the RAGIngestVectorStoreOptions union; shape matches the ingestion class config.
litellm/constants.py Adds three Milvus RAG constants (vector field name, text field name, metric type) following the existing constant pattern for other providers.
litellm/rag/main.py Registers MilvusRAGIngestion under the "milvus" key in INGESTION_REGISTRY, enabling get_ingestion_class("milvus") lookups used by the authorization dispatch.
tests/test_litellm/rag/test_milvus_ingestion.py 16 unit tests covering config validation, auth header logic, auto-create/skip-create flows, error propagation, env-only db/partition, and credential hydration protection — all mocked, no real network calls.
tests/test_litellm/proxy/rag_endpoints/test_rag_endpoints.py Extended with tests covering collection_name normalization/authorization bypass prevention, VIEW_ONLY auto-create guard for Milvus, backward-compatibility for OpenAI/Bedrock provider-native IDs, and provider capability detection — all properly mocked.
tests/vector_store_tests/rag/test_rag_milvus.py Env-gated integration test (skips unless MILVUS_API_BASE is set); follows BaseRAGTest pattern; uses httpx directly for verification queries, consistent with integration test conventions for this folder.

Reviews (5): Last reviewed commit: "fix(rag): block credential hydration fro..." | Re-trigger Greptile

Comment thread litellm/proxy/rag_endpoints/endpoints.py Outdated
Comment thread litellm/proxy/rag_endpoints/endpoints.py
Comment thread litellm/rag/ingestion/milvus_ingestion.py
@Sameerlite

Copy link
Copy Markdown
Contributor

@maycuatroi1 Please resolve all the greptile comments and get the score to 5/5. Also add working proof of this. Thanks!

@maycuatroi1

Copy link
Copy Markdown
Author

@Sameerlite thanks for your comment.

Pushed 66835c8b - resolves all 3 Greptile comments (P0 authz both-fields bypass, P1 provider code in proxy, P2 embed() side-effect + CodeQL import). Proof below.

Working proof - live /rag/ingest against milvusdb/milvus:v2.5.4

curl -X POST $PROXY/rag/ingest -H "Authorization: Bearer $KEY" \
  -F "file=@devproof.txt;type=text/plain" \
  -F 'request={"ingest_options":{"embedding":{"model":"text-embedding-3-small"},
       "vector_store":{"custom_llm_provider":"milvus","collection_name":"pr30388_dev_proof"}}}'
# {"status":"completed","vector_store_id":"pr30388_dev_proof","file_id":"devproof.txt"}

Auto-created collection is queryable (POST /v2/vectordb/entities/query):

{"code":0,"data":[{"chunk_index":0,"filename":"devproof.txt","text":"LiteLLM PR 30388 proof. Photosynthesis ..."}]}

Shows up in the dashboard as:

image image

Tests

pytest tests/test_litellm/rag/test_milvus_ingestion.py -q                          # 16 passed
pytest tests/test_litellm/proxy/rag_endpoints/test_rag_endpoints.py -k Milvus -q   # incl. both-fields bypass test

Comment thread litellm/proxy/rag_endpoints/endpoints.py
Comment thread litellm/rag/ingestion/milvus_ingestion.py
Comment thread litellm/rag/ingestion/milvus_ingestion.py Outdated
@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for the contribution!

We're triggering a Greptile code review on this PR — we'll take a closer look once the results are in!

@greptileai

@Sameerlite

Copy link
Copy Markdown
Contributor

@maycuatroi1 Can you please rebase the PR to litellm_internal_staging? Thanks

Adds write/ingest support for self-hosted Milvus to complement the existing
Milvus search provider. /rag/ingest now accepts custom_llm_provider=milvus.

- MilvusRAGIngestion implements the store() step via the Milvus REST API v2
  (entities/insert), reusing the base upload/ocr/chunk/embed pipeline
- Auto-creates the collection via quick setup (dynamic fields) when missing
- Embeddings generated through litellm embedding API (any provider)
- api_key optional for auth-less self-hosted Milvus; supports db_name/partition
- Registered in INGESTION_REGISTRY; MilvusVectorStoreOptions added to types
- 16 unit tests (mocked REST) + env-gated integration test
Milvus ingestion writes to collection_name (falling back to vector_store_id),
but /rag/ingest only authorized fields named vector_store_id. A request with
custom_llm_provider=milvus and collection_name set to another team's managed
collection bypassed assert_user_can_access_vector_store_id. Normalize
collection_name into vector_store_id before authorization.
Resolves the Greptile review on the Milvus RAG ingestion path:

- P0 (security): vector-store-id normalization for authorization now always
  mirrors collection_name onto vector_store_id for Milvus, not only when
  vector_store_id is absent. A request pairing a collection_name the caller
  cannot access with a vector_store_id they can no longer bypasses
  assert_user_can_access_vector_store_id. Adds a test for the both-fields case.

- P1: removes the provider-specific `custom_llm_provider == "milvus"` branch
  from proxy/rag_endpoints/endpoints.py. BaseRAGIngestion now exposes a
  normalize_authorized_vector_store_id classmethod (no-op by default) that
  MilvusRAGIngestion overrides; the proxy dispatches generically via
  get_ingestion_class.

- P2: removes the embed() side-effect that mutated self.embedding_config on
  first call. The default model is set once in MilvusRAGIngestion.__init__ and
  the class inherits BaseRAGIngestion.embed. Drops the now-unused top-level
  `import litellm` (also clears the CodeQL import/import-from warning).
Require INTERNAL_USER_VIEW_ONLY ingest targets to resolve to an existing
managed vector store. Presence of vector_store_id was insufficient: Milvus
normalization mirrors collection_name onto vector_store_id and unknown ids
pass authorization as provider-native targets, letting a view-only caller
trigger Milvus auto_create_collection for a brand-new collection.
Milvus db_name selects the write target's database namespace but the proxy
only authorizes collection_name/vector_store_id. A caller with access to a
managed collection could set db_name to redirect writes/auto-create into
another Milvus database using the server's credentials, outside the
per-collection authorization boundary.

Resolve db_name from MILVUS_DB_NAME (server-side) only; never from the
request. Drop db_name from MilvusVectorStoreOptions and add a regression
test asserting a request-supplied db_name is ignored.
@Sameerlite

Copy link
Copy Markdown
Contributor

and the score is still 4/5

The view-only ingest guard required every vector_store_id to resolve to a
litellm-managed store, which broke INTERNAL_USER_VIEW_ONLY callers writing to
provider-native ids (e.g. OpenAI vs_*) that are not in the managed registry

Only providers that can create a store on ingest (Milvus with
auto_create_collection) let a view-only caller bring a brand-new store into
existence, so the managed-store requirement now applies only to those. Each
ingestion class declares this via can_auto_create_vector_store and the proxy
dispatches to it instead of hardcoding provider logic. Providers that only
write to a pre-existing store keep accepting their provider-native ids
unchanged

Also drops the banned typing imports from the new milvus_ingestion module so
it stays within the strict-rule budget gate after the rebase onto
litellm_internal_staging
@maycuatroi1

Copy link
Copy Markdown
Author

@Sameerlite pushed ef3fb12 to address the 4/5 concern (the view-only guard being a breaking change for provider-native ids).

The strict guard required every vector_store_id to resolve to a litellm-managed store, which would have broken existing INTERNAL_USER_VIEW_ONLY callers writing to provider-native ids like OpenAI vs_* that are not in the managed registry. The real risk is narrower: only a provider that auto-creates the store on ingest (Milvus with auto_create_collection) lets a view-only caller bring a brand-new store into existence.

So I scoped the guard to that case. Each ingestion class now declares whether it can auto-create via can_auto_create_vector_store, and the proxy enforces the managed-store requirement only when that returns true. Milvus returns true unless auto_create_collection is disabled; everything else inherits the default of false, so OpenAI/Bedrock/etc. keep accepting provider-native ids exactly as before. No provider logic is hardcoded in the proxy; it dispatches to the ingestion class, same pattern as the existing normalize_authorized_vector_store_id hook.

This also answers your other question about the changed test: with the guard scoped, test_internal_user_viewer_rag_ingest_with_vector_store_id_passes_check no longer needs the managed-store mock, so it is reverted to its original form. New coverage went in for the backwards-compat path (a view-only OpenAI provider-native id is allowed), a Milvus auto_create_collection: false case that passes the role check, and unit tests for the per-provider auto-create dispatch; the Milvus auto-create-via-collection_name denial test stays.

The rebase onto litellm_internal_staging also pulled in the strict-rule budget gate, so I converted the new milvus_ingestion module off the banned typing.Any/Dict/List imports to builtin generics; make lint strict budget now passes, and ruff/black/mypy are clean on the touched files.

Comment thread litellm/rag/ingestion/milvus_ingestion.py Outdated
A named credential can carry api_base while leaving api_key unset, which
slips a request-controlled endpoint past the proxy's api_base block. The
constructor then fell back to MILVUS_API_KEY independently, sending the
server token to that endpoint. Only fall back to the env token when
api_base also comes from MILVUS_API_BASE.
Comment thread litellm/rag/ingestion/milvus_ingestion.py Outdated
…s of auto_create flag

can_auto_create_vector_store read the request-supplied auto_create_collection
flag, so a view-only key could set it to false, name any existing unmanaged
collection, and skip the managed-store resolution check in
_assert_view_only_role_cannot_create_vector_store. Report the provider's
capability instead: Milvus can always auto-create, so a view-only target must
always resolve to a managed vector store.
@maycuatroi1

maycuatroi1 commented Jun 16, 2026

Copy link
Copy Markdown
Author

TL;DR (update): any-discipline is red but not a merge blocker; #30574 merged with it failing across ~95 litellm/*.py files. Most findings here are gate false positives (TypedDict fields, await -> Coroutine[Any, Any, T], isinstance(x, dict)); lint (Black, mypy, ruff budget) is green. @mateo-berri is this meant to block, and do you want # any-ok or a gate fix? @Sameerlite flagging so review is not held up.


Heads up on the any-discipline job (the new gate from #30379). This branch predated that PR, so the lint and any-discipline jobs were red only because scripts/type_check_gate.py and scripts/check_any_discipline.py did not exist on the branch yet. I merged the latest litellm_internal_staging to pull those in, which gets lint (Black plus the mypy ceiling) passing again.

With the gate actually running now, it reports 87 LIT009 findings on this PR's changed lines. A good chunk of them look like gate false positives that no concrete typing can resolve, so I wanted to ask how you'd like them handled before adding any suppressions.

The first group is TypedDict field declarations, 9 findings in litellm/types/rag.py. The gate types collection_name: str inside a TypedDict body as Any; a whole-file run flags every field of the pre-existing OpenAIVectorStoreOptions and BedrockVectorStoreOptions the same way, and the functional TypedDict(...) form is flagged too, so there is no declaration syntax that avoids it. The second group is async call sites, 7 findings, where await self._post(...) types the call as Coroutine[Any, Any, T] and those first two Any are intrinsic to every async def. A third recurring one is isinstance(x, dict) narrowing on request payloads, which produces dict[Any, Any] and then taints every .get() or argument use downstream.

The rest are genuine Dict[str, Any] access that I can type concretely. Since the gate is one day old and # any-ok has zero usage anywhere in the tree (CLAUDE.md calls it a last resort), how would you prefer this PR proceeds? Is any-discipline meant to block here, and would you rather I add # any-ok: <reason> for the genuine boundary cases while leaving the TypedDict and async-coroutine flags for a gate fix, or handle it some other way

Use PEP 585/604 builtins (dict, tuple, X | None) in the Milvus ingestion and
RAG endpoint helpers so the strict-rule budget delta (UP006/UP035/UP045) stays
under the lowered ceiling pulled in from staging.
@mateo-berri

Copy link
Copy Markdown
Contributor

Hey thanks for the heads up. I think you can just ignore it for now. I am going to make any-discipline much less strict. It seems to flag on way too many prs currently

@Sameerlite

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

mateo-berri commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

The any-discipline should be more lax now. Let me know if it's a blocker still

@Sameerlite

Copy link
Copy Markdown
Contributor

@maycuatroi1 Still failing lint

@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for the PR! A couple of things to get this over the finish line:

  • The CI checks are currently failing — could you take a look? If any failures are pre-existing or unrelated to your change, a quick note in a comment helps us move faster.

Once those are addressed we'll take another look — appreciate the contribution!

Comment thread litellm/rag/ingestion/milvus_ingestion.py
@maycuatroi1

Copy link
Copy Markdown
Author

@Sameerlite please check. The code now clean and beauty 😄

@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for your contribution! Triggering a code review now.

@greptileai

@maycuatroi1

Copy link
Copy Markdown
Author

@Sameerlite gentle nudge on this one. All 74 CI checks are green now, lint is clean, and the Greptile concerns from the earlier rounds have all been addressed. Whenever you get a chance, could you take another look and let me know if anything else is needed before merge? Happy to make further changes. Thanks!

@Sameerlite
Sameerlite changed the base branch from litellm_internal_staging to litellm_oss_staging_230626 June 23, 2026 13:25
@Sameerlite
Sameerlite merged commit e5a339e into BerriAI:litellm_oss_staging_230626 Jun 23, 2026
75 checks passed
Sameerlite pushed a commit that referenced this pull request Jun 24, 2026
* feat(rag): add Milvus vector store ingestion support

Adds write/ingest support for self-hosted Milvus to complement the existing
Milvus search provider. /rag/ingest now accepts custom_llm_provider=milvus.

- MilvusRAGIngestion implements the store() step via the Milvus REST API v2
  (entities/insert), reusing the base upload/ocr/chunk/embed pipeline
- Auto-creates the collection via quick setup (dynamic fields) when missing
- Embeddings generated through litellm embedding API (any provider)
- api_key optional for auth-less self-hosted Milvus; supports db_name/partition
- Registered in INGESTION_REGISTRY; MilvusVectorStoreOptions added to types
- 16 unit tests (mocked REST) + env-gated integration test

* fix(rag): authorize Milvus collection_name as vector_store_id on ingest

Milvus ingestion writes to collection_name (falling back to vector_store_id),
but /rag/ingest only authorized fields named vector_store_id. A request with
custom_llm_provider=milvus and collection_name set to another team's managed
collection bypassed assert_user_can_access_vector_store_id. Normalize
collection_name into vector_store_id before authorization.

* fix(rag): close Milvus collection_name authz bypass and address review

Resolves the Greptile review on the Milvus RAG ingestion path:

- P0 (security): vector-store-id normalization for authorization now always
  mirrors collection_name onto vector_store_id for Milvus, not only when
  vector_store_id is absent. A request pairing a collection_name the caller
  cannot access with a vector_store_id they can no longer bypasses
  assert_user_can_access_vector_store_id. Adds a test for the both-fields case.

- P1: removes the provider-specific `custom_llm_provider == "milvus"` branch
  from proxy/rag_endpoints/endpoints.py. BaseRAGIngestion now exposes a
  normalize_authorized_vector_store_id classmethod (no-op by default) that
  MilvusRAGIngestion overrides; the proxy dispatches generically via
  get_ingestion_class.

- P2: removes the embed() side-effect that mutated self.embedding_config on
  first call. The default model is set once in MilvusRAGIngestion.__init__ and
  the class inherits BaseRAGIngestion.embed. Drops the now-unused top-level
  `import litellm` (also clears the CodeQL import/import-from warning).

* fix(rag): block view-only role from auto-creating Milvus collections

Require INTERNAL_USER_VIEW_ONLY ingest targets to resolve to an existing
managed vector store. Presence of vector_store_id was insufficient: Milvus
normalization mirrors collection_name onto vector_store_id and unknown ids
pass authorization as provider-native targets, letting a view-only caller
trigger Milvus auto_create_collection for a brand-new collection.

* fix(rag): authorize Milvus db_name via server env only

Milvus db_name selects the write target's database namespace but the proxy
only authorizes collection_name/vector_store_id. A caller with access to a
managed collection could set db_name to redirect writes/auto-create into
another Milvus database using the server's credentials, outside the
per-collection authorization boundary.

Resolve db_name from MILVUS_DB_NAME (server-side) only; never from the
request. Drop db_name from MilvusVectorStoreOptions and add a regression
test asserting a request-supplied db_name is ignored.

* fix(rag): authorize Milvus partition_name via server env only

* fix(rag): scope view-only ingest guard to auto-creating providers

The view-only ingest guard required every vector_store_id to resolve to a
litellm-managed store, which broke INTERNAL_USER_VIEW_ONLY callers writing to
provider-native ids (e.g. OpenAI vs_*) that are not in the managed registry

Only providers that can create a store on ingest (Milvus with
auto_create_collection) let a view-only caller bring a brand-new store into
existence, so the managed-store requirement now applies only to those. Each
ingestion class declares this via can_auto_create_vector_store and the proxy
dispatches to it instead of hardcoding provider logic. Providers that only
write to a pre-existing store keep accepting their provider-native ids
unchanged

Also drops the banned typing imports from the new milvus_ingestion module so
it stays within the strict-rule budget gate after the rebase onto
litellm_internal_staging

* fix(rag): bind Milvus api_key fallback to server-resolved api_base

A named credential can carry api_base while leaving api_key unset, which
slips a request-controlled endpoint past the proxy's api_base block. The
constructor then fell back to MILVUS_API_KEY independently, sending the
server token to that endpoint. Only fall back to the env token when
api_base also comes from MILVUS_API_BASE.

* fix(rag): require managed store for view-only Milvus ingest regardless of auto_create flag

can_auto_create_vector_store read the request-supplied auto_create_collection
flag, so a view-only key could set it to false, name any existing unmanaged
collection, and skip the managed-store resolution check in
_assert_view_only_role_cannot_create_vector_store. Report the provider's
capability instead: Milvus can always auto-create, so a view-only target must
always resolve to a managed vector store.

* style(rag): apply black formatting to Milvus ingest files

* style(rag): modernize typing to satisfy ruff strict-rule budget

Use PEP 585/604 builtins (dict, tuple, X | None) in the Milvus ingestion and
RAG endpoint helpers so the strict-rule budget delta (UP006/UP035/UP045) stays
under the lowered ceiling pulled in from staging.

* style(rag): drop redundant quoted annotations to satisfy UP037 budget

* chore(rag): retrigger CI after transient artifact-download 403

* fix(rag): block credential hydration from overriding authorized write target
Sameerlite pushed a commit that referenced this pull request Jun 29, 2026
* feat(rag): add Milvus vector store ingestion support

Adds write/ingest support for self-hosted Milvus to complement the existing
Milvus search provider. /rag/ingest now accepts custom_llm_provider=milvus.

- MilvusRAGIngestion implements the store() step via the Milvus REST API v2
  (entities/insert), reusing the base upload/ocr/chunk/embed pipeline
- Auto-creates the collection via quick setup (dynamic fields) when missing
- Embeddings generated through litellm embedding API (any provider)
- api_key optional for auth-less self-hosted Milvus; supports db_name/partition
- Registered in INGESTION_REGISTRY; MilvusVectorStoreOptions added to types
- 16 unit tests (mocked REST) + env-gated integration test

* fix(rag): authorize Milvus collection_name as vector_store_id on ingest

Milvus ingestion writes to collection_name (falling back to vector_store_id),
but /rag/ingest only authorized fields named vector_store_id. A request with
custom_llm_provider=milvus and collection_name set to another team's managed
collection bypassed assert_user_can_access_vector_store_id. Normalize
collection_name into vector_store_id before authorization.

* fix(rag): close Milvus collection_name authz bypass and address review

Resolves the Greptile review on the Milvus RAG ingestion path:

- P0 (security): vector-store-id normalization for authorization now always
  mirrors collection_name onto vector_store_id for Milvus, not only when
  vector_store_id is absent. A request pairing a collection_name the caller
  cannot access with a vector_store_id they can no longer bypasses
  assert_user_can_access_vector_store_id. Adds a test for the both-fields case.

- P1: removes the provider-specific `custom_llm_provider == "milvus"` branch
  from proxy/rag_endpoints/endpoints.py. BaseRAGIngestion now exposes a
  normalize_authorized_vector_store_id classmethod (no-op by default) that
  MilvusRAGIngestion overrides; the proxy dispatches generically via
  get_ingestion_class.

- P2: removes the embed() side-effect that mutated self.embedding_config on
  first call. The default model is set once in MilvusRAGIngestion.__init__ and
  the class inherits BaseRAGIngestion.embed. Drops the now-unused top-level
  `import litellm` (also clears the CodeQL import/import-from warning).

* fix(rag): block view-only role from auto-creating Milvus collections

Require INTERNAL_USER_VIEW_ONLY ingest targets to resolve to an existing
managed vector store. Presence of vector_store_id was insufficient: Milvus
normalization mirrors collection_name onto vector_store_id and unknown ids
pass authorization as provider-native targets, letting a view-only caller
trigger Milvus auto_create_collection for a brand-new collection.

* fix(rag): authorize Milvus db_name via server env only

Milvus db_name selects the write target's database namespace but the proxy
only authorizes collection_name/vector_store_id. A caller with access to a
managed collection could set db_name to redirect writes/auto-create into
another Milvus database using the server's credentials, outside the
per-collection authorization boundary.

Resolve db_name from MILVUS_DB_NAME (server-side) only; never from the
request. Drop db_name from MilvusVectorStoreOptions and add a regression
test asserting a request-supplied db_name is ignored.

* fix(rag): authorize Milvus partition_name via server env only

* fix(rag): scope view-only ingest guard to auto-creating providers

The view-only ingest guard required every vector_store_id to resolve to a
litellm-managed store, which broke INTERNAL_USER_VIEW_ONLY callers writing to
provider-native ids (e.g. OpenAI vs_*) that are not in the managed registry

Only providers that can create a store on ingest (Milvus with
auto_create_collection) let a view-only caller bring a brand-new store into
existence, so the managed-store requirement now applies only to those. Each
ingestion class declares this via can_auto_create_vector_store and the proxy
dispatches to it instead of hardcoding provider logic. Providers that only
write to a pre-existing store keep accepting their provider-native ids
unchanged

Also drops the banned typing imports from the new milvus_ingestion module so
it stays within the strict-rule budget gate after the rebase onto
litellm_internal_staging

* fix(rag): bind Milvus api_key fallback to server-resolved api_base

A named credential can carry api_base while leaving api_key unset, which
slips a request-controlled endpoint past the proxy's api_base block. The
constructor then fell back to MILVUS_API_KEY independently, sending the
server token to that endpoint. Only fall back to the env token when
api_base also comes from MILVUS_API_BASE.

* fix(rag): require managed store for view-only Milvus ingest regardless of auto_create flag

can_auto_create_vector_store read the request-supplied auto_create_collection
flag, so a view-only key could set it to false, name any existing unmanaged
collection, and skip the managed-store resolution check in
_assert_view_only_role_cannot_create_vector_store. Report the provider's
capability instead: Milvus can always auto-create, so a view-only target must
always resolve to a managed vector store.

* style(rag): apply black formatting to Milvus ingest files

* style(rag): modernize typing to satisfy ruff strict-rule budget

Use PEP 585/604 builtins (dict, tuple, X | None) in the Milvus ingestion and
RAG endpoint helpers so the strict-rule budget delta (UP006/UP035/UP045) stays
under the lowered ceiling pulled in from staging.

* style(rag): drop redundant quoted annotations to satisfy UP037 budget

* chore(rag): retrigger CI after transient artifact-download 403

* fix(rag): block credential hydration from overriding authorized write target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants