From 7d36c0815f0b3f052dbb73c16177f9823d9fa2ae Mon Sep 17 00:00:00 2001
From: Bigint <69431456+bigint@users.noreply.github.com>
Date: Thu, 21 May 2026 22:55:04 +0530
Subject: [PATCH 1/9] refactor: simplify bigrag platform monorepo
---
api/alembic/versions/0001_initial_schema.py | 71 ---
api/bigrag/app_factory/lifespan.py | 13 +-
api/bigrag/app_factory/routers.py | 2 -
api/bigrag/config.py | 6 +-
api/bigrag/db/models/__init__.py | 2 -
api/bigrag/db/models/collection.py | 10 -
api/bigrag/db/models/vector_migration.py | 51 --
api/bigrag/logging.py | 1 -
api/bigrag/main.py | 5 +-
api/bigrag/middleware/maintenance.py | 2 +-
api/bigrag/models/collection.py | 12 +-
api/bigrag/models/vector_migration.py | 38 --
api/bigrag/routers/admin_realtime.py | 33 --
api/bigrag/routers/admin_vector_migrations.py | 177 ------
api/bigrag/routers/admin_vector_storage.py | 4 +-
api/bigrag/routers/collections.py | 10 +-
api/bigrag/routers/collections_embedding.py | 4 +-
api/bigrag/routers/documents.py | 3 -
api/bigrag/routers/documents_batch.py | 1 -
api/bigrag/routers/documents_global.py | 1 -
api/bigrag/routers/evaluation.py | 1 -
api/bigrag/routers/query.py | 5 -
api/bigrag/routers/vectors.py | 4 +-
api/bigrag/services/backup/exporters.py | 2 -
api/bigrag/services/backup/jobs.py | 15 +-
api/bigrag/services/chat/questions/api.py | 1 -
api/bigrag/services/chat/turn/prepare.py | 1 -
api/bigrag/services/collection_cache.py | 2 -
api/bigrag/services/collection_provision.py | 36 +-
api/bigrag/services/collections.py | 6 +-
api/bigrag/services/connectors/documents.py | 2 -
api/bigrag/services/connectors/manifest.py | 1 -
api/bigrag/services/health.py | 5 -
api/bigrag/services/ingestion_job.py | 3 -
api/bigrag/services/jobs/actors.py | 16 -
api/bigrag/services/jobs/runtime.py | 9 +-
api/bigrag/services/queue_embedding/insert.py | 4 -
.../runtime_setting_specs/vector_store.py | 44 +-
api/bigrag/services/runtime_settings_apply.py | 13 +-
.../services/vector_migration/__init__.py | 17 -
api/bigrag/services/vector_migration/jobs.py | 522 ------------------
api/bigrag/services/vector_store/_util.py | 2 +-
api/bigrag/services/vector_store/base.py | 3 +-
api/bigrag/services/vector_store/facade.py | 229 ++------
api/bigrag/services/vector_store/qdrant.py | 499 -----------------
.../services/vector_store/qdrant_filter.py | 60 --
.../services/vector_store/turbopuffer.py | 32 +-
api/pyproject.toml | 1 -
api/uv.lock | 128 +----
app/src/components/navigation/sidebar.tsx | 2 -
.../collections/collection-form-state.ts | 4 -
.../collections/create-collection-modal.tsx | 17 -
app/src/features/overview/overview-page.tsx | 7 +-
.../settings/instance-settings-helpers.ts | 1 -
app/src/features/settings/settings-layout.ts | 14 +-
app/src/features/settings/tabs/server-tab.tsx | 6 +-
.../vector-storage/vector-migration-panel.tsx | 370 -------------
.../vector-storage/vector-storage-page.tsx | 84 ---
.../vector-storage/vector-storage-route.tsx | 21 -
app/src/hooks/use-collections.ts | 1 -
app/src/hooks/use-vector-migrations.ts | 109 ----
app/src/lib/query-keys.ts | 7 -
app/src/routeTree.gen.ts | 21 -
.../_dashboard.collections.$name.index.tsx | 4 -
.../_dashboard.collections.$name.settings.tsx | 29 +-
.../routes/_dashboard.collections.$name.tsx | 4 -
.../routes/_dashboard.collections.index.tsx | 17 -
app/src/routes/_dashboard.vector-storage.tsx | 16 -
app/src/types/bigrag-api/admin.ts | 2 -
app/src/types/bigrag-api/settings.ts | 28 -
dev.sh | 14 +-
docker-compose.yml | 24 +-
.../src/bigrag/resources/admin/__init__.py | 4 -
.../src/bigrag/resources/admin/realtime.py | 12 -
.../resources/admin/vector_migrations.py | 56 --
sdks/python/src/bigrag/types/__init__.py | 12 +-
sdks/python/src/bigrag/types/admin.py | 37 --
sdks/python/src/bigrag/types/chat.py | 4 +-
sdks/python/src/bigrag/types/collections.py | 12 +-
sdks/python/src/bigrag/types/common.py | 2 -
sdks/python/src/bigrag/types/evaluations.py | 4 +-
sdks/python/src/bigrag/types/query.py | 10 +-
sdks/typescript/src/resources/admin/index.ts | 4 -
.../src/resources/admin/realtime.ts | 11 -
.../src/resources/admin/vector_migrations.ts | 53 --
sdks/typescript/src/types/admin.ts | 34 --
sdks/typescript/src/types/collections.ts | 4 -
sdks/typescript/src/types/common.ts | 2 -
88 files changed, 163 insertions(+), 3009 deletions(-)
delete mode 100644 api/bigrag/db/models/vector_migration.py
delete mode 100644 api/bigrag/models/vector_migration.py
delete mode 100644 api/bigrag/routers/admin_vector_migrations.py
delete mode 100644 api/bigrag/services/vector_migration/__init__.py
delete mode 100644 api/bigrag/services/vector_migration/jobs.py
delete mode 100644 api/bigrag/services/vector_store/qdrant.py
delete mode 100644 api/bigrag/services/vector_store/qdrant_filter.py
delete mode 100644 app/src/features/vector-storage/vector-migration-panel.tsx
delete mode 100644 app/src/features/vector-storage/vector-storage-page.tsx
delete mode 100644 app/src/features/vector-storage/vector-storage-route.tsx
delete mode 100644 app/src/hooks/use-vector-migrations.ts
delete mode 100644 app/src/routes/_dashboard.vector-storage.tsx
delete mode 100644 sdks/python/src/bigrag/resources/admin/vector_migrations.py
delete mode 100644 sdks/typescript/src/resources/admin/vector_migrations.ts
diff --git a/api/alembic/versions/0001_initial_schema.py b/api/alembic/versions/0001_initial_schema.py
index 3658d651..fff94d0d 100644
--- a/api/alembic/versions/0001_initial_schema.py
+++ b/api/alembic/versions/0001_initial_schema.py
@@ -260,7 +260,6 @@ def upgrade() -> None:
sa.Column("embedding_api_key", bigrag.services.crypto.EncryptedString(), nullable=True),
sa.Column("embedding_base_url", sa.Text(), nullable=True),
sa.Column("embedding_preset_id", sa.Uuid(), nullable=True),
- sa.Column("vector_store_provider", sa.Text(), server_default="qdrant", nullable=False),
sa.Column("dimension", sa.Integer(), server_default=sa.text("1536"), nullable=False),
sa.Column("chunk_size", sa.Integer(), server_default=sa.text("512"), nullable=False),
sa.Column("chunk_overlap", sa.Integer(), server_default=sa.text("50"), nullable=False),
@@ -283,7 +282,6 @@ def upgrade() -> None:
server_default=sa.text("false"),
nullable=False,
),
- sa.Column("index_type", sa.Text(), server_default="HNSW", nullable=False),
sa.Column("tenant_field", sa.Text(), nullable=True),
sa.Column("metadata_schema", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column(
@@ -317,66 +315,6 @@ def upgrade() -> None:
unique=False,
)
op.create_index("idx_collections_name", "collections", ["name"], unique=False)
- op.create_table(
- "vector_migration_jobs",
- sa.Column("id", sa.Uuid(), nullable=False),
- sa.Column("collection_id", sa.Uuid(), nullable=True),
- sa.Column("collection_name", sa.Text(), nullable=False),
- sa.Column("source_provider", sa.Text(), nullable=False),
- sa.Column("target_provider", sa.Text(), nullable=False),
- sa.Column("status", sa.Text(), server_default="pending", nullable=False),
- sa.Column("phase", sa.Text(), server_default="queued", nullable=False),
- sa.Column("progress", sa.Double(), server_default=sa.text("0"), nullable=False),
- sa.Column("copied_points", sa.Integer(), server_default=sa.text("0"), nullable=False),
- sa.Column("total_points", sa.Integer(), nullable=True),
- sa.Column(
- "details",
- postgresql.JSONB(astext_type=sa.Text()),
- server_default=sa.text("'{}'::jsonb"),
- nullable=False,
- ),
- sa.Column("error_message", sa.Text(), nullable=True),
- sa.Column("created_by", sa.Uuid(), nullable=True),
- sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
- sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
- sa.Column(
- "created_at",
- sa.DateTime(timezone=True),
- server_default=sa.text("now()"),
- nullable=False,
- ),
- sa.Column(
- "updated_at",
- sa.DateTime(timezone=True),
- server_default=sa.text("now()"),
- nullable=False,
- ),
- sa.CheckConstraint(
- "status IN ('pending', 'running', 'canceling', 'succeeded', 'failed')",
- name="vector_migration_jobs_status_check",
- ),
- sa.ForeignKeyConstraint(["collection_id"], ["collections.id"], ondelete="SET NULL"),
- sa.ForeignKeyConstraint(["created_by"], ["users.id"], ondelete="SET NULL"),
- sa.PrimaryKeyConstraint("id"),
- )
- op.create_index(
- "idx_vector_migration_jobs_collection",
- "vector_migration_jobs",
- ["collection_name"],
- unique=False,
- )
- op.create_index(
- "idx_vector_migration_jobs_created_at_id",
- "vector_migration_jobs",
- [sa.literal_column("created_at DESC"), sa.literal_column("id DESC")],
- unique=False,
- )
- op.create_index(
- "idx_vector_migration_jobs_status",
- "vector_migration_jobs",
- ["status"],
- unique=False,
- )
op.create_table(
"connector_accounts",
sa.Column("id", sa.Uuid(), nullable=False),
@@ -1170,11 +1108,6 @@ def upgrade() -> None:
"webhook_deliveries",
"status IN ('pending', 'delivered', 'failed')",
)
- op.create_check_constraint(
- "collections_vector_store_provider_check",
- "collections",
- "vector_store_provider IN ('qdrant', 'turbopuffer')",
- )
op.create_check_constraint(
"embedding_presets_provider_check",
"embedding_presets",
@@ -1266,10 +1199,6 @@ def downgrade() -> None:
op.drop_table("connector_accounts")
op.drop_index("idx_collections_name", table_name="collections")
op.drop_index("idx_collections_created_at_id", table_name="collections")
- op.drop_index("idx_vector_migration_jobs_status", table_name="vector_migration_jobs")
- op.drop_index("idx_vector_migration_jobs_created_at_id", table_name="vector_migration_jobs")
- op.drop_index("idx_vector_migration_jobs_collection", table_name="vector_migration_jobs")
- op.drop_table("vector_migration_jobs")
op.drop_table("collections")
op.drop_index("idx_backup_jobs_status", table_name="backup_jobs")
op.drop_index("idx_backup_jobs_created_at_id", table_name="backup_jobs")
diff --git a/api/bigrag/app_factory/lifespan.py b/api/bigrag/app_factory/lifespan.py
index 12034bf1..0c31aa69 100644
--- a/api/bigrag/app_factory/lifespan.py
+++ b/api/bigrag/app_factory/lifespan.py
@@ -52,21 +52,16 @@ async def lifespan(app: FastAPI):
runtime = await runtime_settings.get_values(
[
"ingestion_workers",
- "qdrant_connect_timeout_seconds",
- "qdrant_required",
- "qdrant_search_ef",
- "qdrant_url",
"turbopuffer_api_key",
+ "turbopuffer_base_url",
"turbopuffer_namespace_prefix",
"turbopuffer_region",
]
)
vector_store.configure(
- qdrant_url=runtime["qdrant_url"],
- connect_timeout_seconds=runtime["qdrant_connect_timeout_seconds"],
- search_ef=runtime["qdrant_search_ef"],
turbopuffer_api_key=runtime["turbopuffer_api_key"],
+ turbopuffer_base_url=runtime["turbopuffer_base_url"],
turbopuffer_region=runtime["turbopuffer_region"],
turbopuffer_namespace_prefix=runtime["turbopuffer_namespace_prefix"],
)
@@ -76,12 +71,10 @@ async def lifespan(app: FastAPI):
except Exception as exc:
logger.warning(
"Vector store startup connection failed; API will start degraded",
- provider=vector_store.provider,
+ provider="turbopuffer",
error_type=exc.__class__.__name__,
error=str(exc),
)
- if runtime["qdrant_required"]:
- raise
app.state.vector_store = vector_store
storage = await init_storage_from_runtime(upload_dir=s.upload_dir)
diff --git a/api/bigrag/app_factory/routers.py b/api/bigrag/app_factory/routers.py
index d1dba1aa..dc2033fc 100644
--- a/api/bigrag/app_factory/routers.py
+++ b/api/bigrag/app_factory/routers.py
@@ -15,7 +15,6 @@ def include_all_routers(app: FastAPI) -> None:
from bigrag.routers.admin_realtime import router as admin_realtime_router
from bigrag.routers.admin_settings import router as admin_settings_router
from bigrag.routers.admin_users import router as admin_users_router
- from bigrag.routers.admin_vector_migrations import router as admin_vector_migrations_router
from bigrag.routers.admin_vector_storage import router as admin_vector_storage_router
from bigrag.routers.analytics import router as analytics_router
from bigrag.routers.auth import router as auth_router
@@ -51,7 +50,6 @@ def include_all_routers(app: FastAPI) -> None:
app.include_router(admin_settings_router)
app.include_router(admin_access_router)
app.include_router(admin_vector_storage_router)
- app.include_router(admin_vector_migrations_router)
app.include_router(admin_realtime_router)
app.include_router(mcp_servers_router)
app.include_router(admin_audit_router)
diff --git a/api/bigrag/config.py b/api/bigrag/config.py
index 9eacab94..5731b974 100644
--- a/api/bigrag/config.py
+++ b/api/bigrag/config.py
@@ -27,12 +27,8 @@ class Settings(BaseSettings):
db_pool_max: int = 50
migration_timeout_seconds: int = 60
- qdrant_url: str = "http://localhost:6333"
- qdrant_connect_timeout_seconds: int = 10
- qdrant_required: bool = False
- qdrant_prefer_grpc: bool = False
- qdrant_grpc_port: int = 6334
turbopuffer_api_key: str | None = None
+ turbopuffer_base_url: str | None = None
turbopuffer_region: str = "aws-us-east-1"
turbopuffer_namespace_prefix: str = "bigrag_"
diff --git a/api/bigrag/db/models/__init__.py b/api/bigrag/db/models/__init__.py
index b9a89dce..1b41b899 100644
--- a/api/bigrag/db/models/__init__.py
+++ b/api/bigrag/db/models/__init__.py
@@ -19,7 +19,6 @@
from bigrag.db.models.instance import InstanceSetting, MaintenanceLock
from bigrag.db.models.observability import AccessLog, AuditLog, BackupJob, QueryLog
from bigrag.db.models.preference import UserPreference
-from bigrag.db.models.vector_migration import VectorMigrationJob
from bigrag.db.models.webhook import Webhook, WebhookDelivery
__all__ = [
@@ -46,7 +45,6 @@
"User",
"UserSession",
"UserPreference",
- "VectorMigrationJob",
"Webhook",
"WebhookDelivery",
]
diff --git a/api/bigrag/db/models/collection.py b/api/bigrag/db/models/collection.py
index 4fda5d81..b6bac6a6 100644
--- a/api/bigrag/db/models/collection.py
+++ b/api/bigrag/db/models/collection.py
@@ -14,10 +14,6 @@
class Collection(Base):
__tablename__ = "collections"
__table_args__ = (
- sa.CheckConstraint(
- "vector_store_provider IN ('qdrant', 'turbopuffer')",
- name="collections_vector_store_provider_check",
- ),
sa.Index("idx_collections_name", "name"),
sa.Index("idx_collections_created_at_id", sa.desc("created_at"), sa.desc("id")),
)
@@ -37,11 +33,6 @@ class Collection(Base):
sa.ForeignKey("embedding_presets.id", ondelete="RESTRICT"),
nullable=True,
)
- vector_store_provider: Mapped[str] = mapped_column(
- sa.Text,
- nullable=False,
- server_default="qdrant",
- )
dimension: Mapped[int] = mapped_column(
sa.Integer, nullable=False, server_default=sa.text("1536")
)
@@ -75,7 +66,6 @@ class Collection(Base):
multimodal_enrichment_enabled: Mapped[bool] = mapped_column(
sa.Boolean, nullable=False, server_default=sa.false()
)
- index_type: Mapped[str] = mapped_column(sa.Text, nullable=False, server_default="HNSW")
tenant_field: Mapped[str | None] = mapped_column(sa.Text)
metadata_schema: Mapped[dict | None] = mapped_column(JSONB)
meta: Mapped[dict] = mapped_column(
diff --git a/api/bigrag/db/models/vector_migration.py b/api/bigrag/db/models/vector_migration.py
deleted file mode 100644
index d9f402c0..00000000
--- a/api/bigrag/db/models/vector_migration.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from __future__ import annotations
-
-from datetime import datetime
-from uuid import UUID
-
-import sqlalchemy as sa
-from sqlalchemy.dialects.postgresql import JSONB
-from sqlalchemy.orm import Mapped, mapped_column
-
-from bigrag.db.base import TS, Base, TSupd, UUIDpk
-
-
-class VectorMigrationJob(Base):
- __tablename__ = "vector_migration_jobs"
- __table_args__ = (
- sa.CheckConstraint(
- "status IN ('pending', 'running', 'canceling', 'succeeded', 'failed')",
- name="vector_migration_jobs_status_check",
- ),
- sa.Index("idx_vector_migration_jobs_collection", "collection_name"),
- sa.Index("idx_vector_migration_jobs_status", "status"),
- sa.Index(
- "idx_vector_migration_jobs_created_at_id",
- sa.desc("created_at"),
- sa.desc("id"),
- ),
- )
-
- id: Mapped[UUIDpk]
- collection_id: Mapped[UUID | None] = mapped_column(
- sa.ForeignKey("collections.id", ondelete="SET NULL")
- )
- collection_name: Mapped[str] = mapped_column(sa.Text, nullable=False)
- source_provider: Mapped[str] = mapped_column(sa.Text, nullable=False)
- target_provider: Mapped[str] = mapped_column(sa.Text, nullable=False)
- status: Mapped[str] = mapped_column(sa.Text, nullable=False, server_default="pending")
- phase: Mapped[str] = mapped_column(sa.Text, nullable=False, server_default="queued")
- progress: Mapped[float] = mapped_column(sa.Double, nullable=False, server_default=sa.text("0"))
- copied_points: Mapped[int] = mapped_column(
- sa.Integer, nullable=False, server_default=sa.text("0")
- )
- total_points: Mapped[int | None] = mapped_column(sa.Integer)
- details: Mapped[dict] = mapped_column(
- JSONB, nullable=False, server_default=sa.text("'{}'::jsonb")
- )
- error_message: Mapped[str | None] = mapped_column(sa.Text)
- created_by: Mapped[UUID | None] = mapped_column(sa.ForeignKey("users.id", ondelete="SET NULL"))
- started_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
- completed_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
- created_at: Mapped[TS]
- updated_at: Mapped[TSupd]
diff --git a/api/bigrag/logging.py b/api/bigrag/logging.py
index a0a0dad4..03e7d6a1 100644
--- a/api/bigrag/logging.py
+++ b/api/bigrag/logging.py
@@ -255,7 +255,6 @@ def configure_logging(log_level: str = "debug", log_format: str = "text") -> Non
"hpack",
"httpcore",
"httpx",
- "qdrant_client",
"uvicorn.access",
):
logging.getLogger(name).setLevel(logging.WARNING)
diff --git a/api/bigrag/main.py b/api/bigrag/main.py
index e4d31f52..b8adf430 100644
--- a/api/bigrag/main.py
+++ b/api/bigrag/main.py
@@ -47,7 +47,7 @@ def create_app(settings_override: Settings | None = None) -> FastAPI:
app = FastAPI(
title="bigRAG",
- description="Self-hostable RAG platform with Docling + Qdrant",
+ description="Self-hostable RAG platform with Docling + turbopuffer",
version=__version__,
lifespan=lifespan,
default_response_class=ORJSONResponse,
@@ -79,7 +79,6 @@ def cli() -> None:
parser.add_argument("--host", help="Server host")
parser.add_argument("--port", type=int, help="Server port")
parser.add_argument("--database-url", help="Postgres connection URL")
- parser.add_argument("--qdrant-url", help="Qdrant connection URL")
parser.add_argument("--redis-url", help="Redis connection URL")
args = parser.parse_args()
@@ -92,8 +91,6 @@ def cli() -> None:
overrides["port"] = args.port
if args.database_url is not None:
overrides["database_url"] = args.database_url
- if args.qdrant_url is not None:
- overrides["qdrant_url"] = args.qdrant_url
if args.redis_url is not None:
overrides["redis_url"] = args.redis_url
for key, value in overrides.items():
diff --git a/api/bigrag/middleware/maintenance.py b/api/bigrag/middleware/maintenance.py
index 4a5e134b..9310c02a 100644
--- a/api/bigrag/middleware/maintenance.py
+++ b/api/bigrag/middleware/maintenance.py
@@ -7,7 +7,7 @@
from bigrag.services.maintenance import active_lock
SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}
-CONTROL_PATH_PREFIXES = ("/v1/admin/backups", "/v1/admin/vector-storage/migrations")
+CONTROL_PATH_PREFIXES = ("/v1/admin/backups",)
def _is_control_path(path: str) -> bool:
diff --git a/api/bigrag/models/collection.py b/api/bigrag/models/collection.py
index 4754965d..cee4226f 100644
--- a/api/bigrag/models/collection.py
+++ b/api/bigrag/models/collection.py
@@ -4,14 +4,11 @@
from pydantic import BaseModel, Field, model_validator
-from bigrag.services.vector_store.base import VectorStoreProvider
-
class CreateCollectionRequest(BaseModel):
name: str = Field(min_length=1, max_length=128, pattern=r"^[a-zA-Z][a-zA-Z0-9_]*$")
description: str = ""
embedding_preset_id: str | None = None
- vector_store_provider: VectorStoreProvider = "qdrant"
embedding_provider: str | None = None
embedding_model: str | None = None
embedding_api_key: str | None = None
@@ -24,16 +21,11 @@ class CreateCollectionRequest(BaseModel):
pattern=r"^(paragraph|recursive)$",
description="Chunking algorithm: paragraph (default) or recursive.",
)
- index_type: str = Field(
- default="HNSW",
- pattern=r"^HNSW$",
- description="Vector index preference. Qdrant uses HNSW for dense-vector search.",
- )
tenant_field: str | None = Field(
default=None,
max_length=64,
description=(
- "Optional metadata field name to index for tenant-aware Qdrant "
+ "Optional metadata field name to index for tenant-aware "
"payload filtering in multi-tenant deployments."
),
)
@@ -92,12 +84,10 @@ class CollectionResponse(BaseModel):
description: str
embedding_provider: str
embedding_model: str
- vector_store_provider: VectorStoreProvider
dimension: int
chunk_size: int
chunk_overlap: int
chunk_strategy: str = "paragraph"
- index_type: str = "HNSW"
tenant_field: str | None = None
has_metadata_schema: bool = False
document_count: int
diff --git a/api/bigrag/models/vector_migration.py b/api/bigrag/models/vector_migration.py
deleted file mode 100644
index 71954ee9..00000000
--- a/api/bigrag/models/vector_migration.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from __future__ import annotations
-
-from datetime import datetime
-
-from pydantic import BaseModel, Field
-
-from bigrag.services.vector_store.base import VectorStoreProvider
-
-
-class VectorMigrationCreateRequest(BaseModel):
- collection: str = Field(min_length=1, max_length=128)
- target_provider: VectorStoreProvider
-
-
-class VectorMigrationJobResponse(BaseModel):
- id: str
- collection_id: str | None
- collection_name: str
- source_provider: VectorStoreProvider
- target_provider: VectorStoreProvider
- status: str
- phase: str
- progress: float
- copied_points: int
- total_points: int | None
- details: dict
- error_message: str | None = None
- created_by: str | None = None
- started_at: datetime | None = None
- completed_at: datetime | None = None
- created_at: datetime
- updated_at: datetime
-
-
-class VectorMigrationJobListResponse(BaseModel):
- jobs: list[VectorMigrationJobResponse]
- total: int | None = None
- next_cursor: str | None = None
diff --git a/api/bigrag/routers/admin_realtime.py b/api/bigrag/routers/admin_realtime.py
index 61e4184d..1b7aa76d 100644
--- a/api/bigrag/routers/admin_realtime.py
+++ b/api/bigrag/routers/admin_realtime.py
@@ -11,7 +11,6 @@
from bigrag.routers.admin_access import access_overview, list_access_logs
from bigrag.routers.admin_audit import list_audit_log
from bigrag.routers.admin_backups import list_backup_jobs
-from bigrag.routers.admin_vector_migrations import list_vector_migration_jobs
from bigrag.routers.collections import get_collection_stats
from bigrag.routers.connectors import connector_sources, connector_sync_jobs
from bigrag.routers.documents import get_document, list_documents
@@ -37,7 +36,6 @@
ACTIVE_SYNC_JOB_STATUSES = {"pending", "running"}
ACTIVE_BACKUP_JOB_STATUSES = {"pending", "running"}
-ACTIVE_VECTOR_MIGRATION_STATUSES = {"pending", "running", "canceling"}
def _parse_document_ids(document_ids: list[str]) -> list[str]:
@@ -84,12 +82,6 @@ def _backup_jobs_interval(payload: Any | None) -> float:
return 2.0 if active else 15.0
-def _vector_migration_jobs_interval(payload: Any | None) -> float:
- jobs = getattr(payload, "jobs", []) if payload is not None else []
- active = any(getattr(job, "status", None) in ACTIVE_VECTOR_MIGRATION_STATUSES for job in jobs)
- return 2.0 if active else 15.0
-
-
@router.get("/collections/{collection_name}/documents", response_class=StreamingResponse)
async def collection_documents_stream(
collection_name: str,
@@ -283,31 +275,6 @@ async def load():
return _interval_response(topic, load, _backup_jobs_interval)
-@router.get("/vector-migrations", response_class=StreamingResponse)
-async def vector_migration_jobs_stream(
- collection: str | None = Query(default=None, max_length=128),
- limit: int = Query(default=20, ge=1, le=100),
- offset: int = Query(default=0, ge=0),
- user: dict = Depends(require_admin_session),
-):
- topic = f"vector-migrations:{collection or 'all'}:{limit}:{offset}"
-
- async def load():
- return await _with_session(
- lambda session: list_vector_migration_jobs(
- collection=collection,
- limit=limit,
- offset=offset,
- cursor=None,
- include_total=False,
- _=user,
- session=session,
- )
- )
-
- return _interval_response(topic, load, _vector_migration_jobs_interval)
-
-
@router.get("/access/overview", response_class=StreamingResponse)
async def access_overview_stream(
window_days: int = Query(default=7, ge=1, le=90),
diff --git a/api/bigrag/routers/admin_vector_migrations.py b/api/bigrag/routers/admin_vector_migrations.py
deleted file mode 100644
index aa623fec..00000000
--- a/api/bigrag/routers/admin_vector_migrations.py
+++ /dev/null
@@ -1,177 +0,0 @@
-from __future__ import annotations
-
-from uuid import UUID
-
-import sqlalchemy as sa
-from fastapi import APIRouter, Depends, HTTPException, Query, Request
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from bigrag.db.models import VectorMigrationJob
-from bigrag.db.session import get_session
-from bigrag.middleware.auth import require_admin_session
-from bigrag.models import StatusResponse
-from bigrag.models.vector_migration import (
- VectorMigrationCreateRequest,
- VectorMigrationJobListResponse,
- VectorMigrationJobResponse,
-)
-from bigrag.routers import uuid_or_404
-from bigrag.services import audit
-from bigrag.services.error_sanitize import sanitize_message_text
-from bigrag.services.jobs.actors import enqueue_vector_migration_job
-from bigrag.services.pagination import apply_cursor, build_response_cursor, decode_cursor_or_400
-from bigrag.services.vector_migration import (
- VectorMigrationConflictError,
- VectorMigrationError,
- create_vector_migration_job,
- delete_vector_migration_job,
-)
-
-router = APIRouter(
- prefix="/v1/admin/vector-storage/migrations",
- tags=["admin:vector-storage"],
-)
-
-
-def vector_migration_job_response(job: VectorMigrationJob) -> VectorMigrationJobResponse:
- return VectorMigrationJobResponse(
- id=str(job.id),
- collection_id=str(job.collection_id) if job.collection_id else None,
- collection_name=job.collection_name,
- source_provider=job.source_provider,
- target_provider=job.target_provider,
- status=job.status,
- phase=job.phase,
- progress=job.progress,
- copied_points=job.copied_points,
- total_points=job.total_points,
- details=job.details or {},
- error_message=job.error_message,
- created_by=str(job.created_by) if job.created_by else None,
- started_at=job.started_at,
- completed_at=job.completed_at,
- created_at=job.created_at,
- updated_at=job.updated_at,
- )
-
-
-@router.get("", response_model=VectorMigrationJobListResponse)
-async def list_vector_migration_jobs(
- collection: str | None = Query(default=None, max_length=128),
- limit: int = Query(default=20, ge=1, le=100),
- offset: int = Query(default=0, ge=0),
- cursor: str | None = Query(default=None),
- include_total: bool = Query(default=False),
- _: dict = Depends(require_admin_session),
- session: AsyncSession = Depends(get_session),
-) -> VectorMigrationJobListResponse:
- cursor_tuple = decode_cursor_or_400(cursor)
- stmt = sa.select(VectorMigrationJob).order_by(
- VectorMigrationJob.created_at.desc(),
- VectorMigrationJob.id.desc(),
- )
- count_stmt = sa.select(sa.func.count()).select_from(VectorMigrationJob)
- if collection:
- stmt = stmt.where(VectorMigrationJob.collection_name == collection)
- count_stmt = count_stmt.where(VectorMigrationJob.collection_name == collection)
-
- if cursor_tuple is not None:
- stmt = apply_cursor(
- stmt,
- VectorMigrationJob.created_at,
- VectorMigrationJob.id,
- cursor_tuple,
- ).limit(limit + 1)
- else:
- stmt = stmt.limit(limit + 1).offset(offset)
-
- rows = (await session.scalars(stmt)).all()
- page, next_cursor = build_response_cursor(list(rows), "created_at", "id", limit)
-
- total: int | None = None
- if include_total:
- total = (await session.scalar(count_stmt)) or 0
-
- return VectorMigrationJobListResponse(
- jobs=[vector_migration_job_response(job) for job in page],
- total=total,
- next_cursor=next_cursor,
- )
-
-
-@router.get("/{migration_id}", response_model=VectorMigrationJobResponse)
-async def get_vector_migration_job(
- migration_id: str,
- _: dict = Depends(require_admin_session),
- session: AsyncSession = Depends(get_session),
-) -> VectorMigrationJobResponse:
- try:
- target_id = UUID(migration_id)
- except ValueError as exc:
- raise HTTPException(status_code=400, detail="Invalid migration_id") from exc
- job = await session.get(VectorMigrationJob, target_id)
- if job is None:
- raise HTTPException(status_code=404, detail="Vector migration job not found")
- return vector_migration_job_response(job)
-
-
-@router.post("", response_model=VectorMigrationJobResponse, status_code=201)
-async def start_vector_migration_job(
- body: VectorMigrationCreateRequest,
- request: Request,
- admin: dict = Depends(require_admin_session),
-) -> VectorMigrationJobResponse:
- user_id = UUID(admin["id"]) if admin.get("id") else None
- try:
- job = await create_vector_migration_job(
- collection_name=body.collection,
- target_provider=body.target_provider,
- created_by=user_id,
- )
- except VectorMigrationConflictError as exc:
- raise HTTPException(
- status_code=409,
- detail=sanitize_message_text(str(exc)) or "Vector migration cannot be started.",
- ) from exc
- except VectorMigrationError as exc:
- raise HTTPException(
- status_code=400,
- detail=sanitize_message_text(str(exc)) or "Vector migration cannot be started.",
- ) from exc
- audit.record(
- request,
- user=admin,
- action="vector_migration.requested",
- resource_type="vector_migration_job",
- resource_id=str(job.id),
- metadata={
- "collection": job.collection_name,
- "source_provider": job.source_provider,
- "target_provider": job.target_provider,
- },
- )
- enqueue_vector_migration_job(str(job.id))
- return vector_migration_job_response(job)
-
-
-@router.delete("/{migration_id}", response_model=StatusResponse)
-async def delete_vector_migration_job_route(
- migration_id: str,
- request: Request,
- admin: dict = Depends(require_admin_session),
-) -> StatusResponse:
- target_id = uuid_or_404(migration_id, "Vector migration job")
- result = await delete_vector_migration_job(target_id)
- if result is None:
- raise HTTPException(status_code=404, detail="Vector migration job not found")
- audit.record(
- request,
- user=admin,
- action="vector_migration.delete",
- resource_type="vector_migration_job",
- resource_id=migration_id,
- metadata={"result": result},
- )
- if result == "stop_requested":
- return StatusResponse(status="ok", message="Vector migration stop requested")
- return StatusResponse(status="ok", message="Vector migration deleted")
diff --git a/api/bigrag/routers/admin_vector_storage.py b/api/bigrag/routers/admin_vector_storage.py
index 0fffd917..70d70d12 100644
--- a/api/bigrag/routers/admin_vector_storage.py
+++ b/api/bigrag/routers/admin_vector_storage.py
@@ -22,7 +22,6 @@ async def vector_storage_overview(
await session.execute(
sa.select(
Collection.name,
- Collection.vector_store_provider,
sa.func.count(Document.id).label("documents"),
sa.func.coalesce(sa.func.sum(Document.chunk_count), 0).label("chunks"),
sa.func.coalesce(sa.func.sum(Document.file_size), 0).label("bytes"),
@@ -35,12 +34,11 @@ async def vector_storage_overview(
collections = [
{
"name": name,
- "provider": provider,
"documents": int(documents or 0),
"chunks": int(chunks or 0),
"bytes": int(bytes_ or 0),
}
- for name, provider, documents, chunks, bytes_ in rows
+ for name, documents, chunks, bytes_ in rows
]
totals = {
"collections": len(collections),
diff --git a/api/bigrag/routers/collections.py b/api/bigrag/routers/collections.py
index 087ea0d4..381a0663 100644
--- a/api/bigrag/routers/collections.py
+++ b/api/bigrag/routers/collections.py
@@ -49,12 +49,10 @@ def _collection_response(c: Collection) -> CollectionResponse:
description=c.description,
embedding_provider=c.embedding_provider,
embedding_model=c.embedding_model,
- vector_store_provider=c.vector_store_provider,
dimension=c.dimension,
chunk_size=c.chunk_size,
chunk_overlap=c.chunk_overlap,
chunk_strategy=c.chunk_strategy,
- index_type=c.index_type,
tenant_field=c.tenant_field,
has_metadata_schema=bool(c.metadata_schema),
document_count=c.document_count,
@@ -125,7 +123,6 @@ async def create_collection(
logger.info(
"create collection",
name=body.name,
- vector_store_provider=body.vector_store_provider,
provider=body.embedding_provider,
model=body.embedding_model,
)
@@ -232,14 +229,12 @@ async def create_collection(
collection = Collection(
name=body.name,
description=body.description,
- vector_store_provider=body.vector_store_provider,
embedding_provider=provider,
embedding_model=model,
dimension=dimension,
chunk_size=body.chunk_size,
chunk_overlap=body.chunk_overlap,
chunk_strategy=body.chunk_strategy,
- index_type=body.index_type,
tenant_field=body.tenant_field,
meta=body.metadata,
metadata_schema=body.metadata_schema,
@@ -260,11 +255,11 @@ async def create_collection(
await session.commit()
except IntegrityError as e:
await session.rollback()
- await vector_store.delete_collection(body.name, provider=body.vector_store_provider)
+ await vector_store.delete_collection(body.name)
raise HTTPException(status_code=409, detail="Collection already exists") from e
except Exception:
await session.rollback()
- await vector_store.delete_collection(body.name, provider=body.vector_store_provider)
+ await vector_store.delete_collection(body.name)
raise
await session.refresh(collection)
await collection_cache.invalidate(body.name)
@@ -285,7 +280,6 @@ async def create_collection(
metadata={
"name": body.name,
"provider": provider,
- "vector_store_provider": body.vector_store_provider,
"model": model,
"dimension": dimension,
},
diff --git a/api/bigrag/routers/collections_embedding.py b/api/bigrag/routers/collections_embedding.py
index 396a5451..76bcc6de 100644
--- a/api/bigrag/routers/collections_embedding.py
+++ b/api/bigrag/routers/collections_embedding.py
@@ -14,6 +14,7 @@
from bigrag.services.ingestion_job import create_ingestion_job
from bigrag.services.queue import ingestion_queue
from bigrag.services.retrieval import invalidate_collection_query_cache
+from bigrag.services.vector_store import vector_store
logger = get_logger("bigrag.routers.collections_embedding")
@@ -47,7 +48,6 @@ async def reembed_collection(
"chunk_size": collection.chunk_size,
"chunk_overlap": collection.chunk_overlap,
"chunk_strategy": collection.chunk_strategy or "paragraph",
- "vector_store_provider": collection.vector_store_provider,
"tenant_field": collection.tenant_field,
}
jobs = [
@@ -61,6 +61,8 @@ async def reembed_collection(
]
doc_ids = [doc_id for doc_id, _ in docs]
+ for doc_id in doc_ids:
+ await vector_store.delete_by_document(name, str(doc_id))
await session.execute(
sa.update(Document)
.where(Document.id.in_(doc_ids))
diff --git a/api/bigrag/routers/documents.py b/api/bigrag/routers/documents.py
index 3c14e52f..19ac88c3 100644
--- a/api/bigrag/routers/documents.py
+++ b/api/bigrag/routers/documents.py
@@ -286,7 +286,6 @@ async def delete_document(
await vector_store.delete_by_document(
collection_name,
document_id,
- provider=collection.get("vector_store_provider"),
)
storage = get_storage()
await storage.delete(file_path)
@@ -333,7 +332,6 @@ async def reprocess_document(
await vector_store.delete_by_document(
collection_name,
document_id,
- provider=collection.get("vector_store_provider"),
)
doc.status = "pending"
@@ -451,7 +449,6 @@ async def get_document_chunks(
document_id,
limit=limit,
offset=offset,
- provider=collection.get("vector_store_provider"),
)
return {"chunks": chunks, "total": total}
diff --git a/api/bigrag/routers/documents_batch.py b/api/bigrag/routers/documents_batch.py
index 5c52d16d..8a18bac9 100644
--- a/api/bigrag/routers/documents_batch.py
+++ b/api/bigrag/routers/documents_batch.py
@@ -241,7 +241,6 @@ async def _delete_one(doc_id: str, doc: Document) -> bool:
await vector_store.delete_by_document(
collection_name,
doc_id,
- provider=collection.get("vector_store_provider"),
)
storage = get_storage()
await storage.delete(doc.file_path)
diff --git a/api/bigrag/routers/documents_global.py b/api/bigrag/routers/documents_global.py
index 881f8c5d..8c72c566 100644
--- a/api/bigrag/routers/documents_global.py
+++ b/api/bigrag/routers/documents_global.py
@@ -60,6 +60,5 @@ async def get_document_chunks_global(
document_id,
limit=limit,
offset=offset,
- provider=collection.get("vector_store_provider"),
)
return {"chunks": chunks, "total": total}
diff --git a/api/bigrag/routers/evaluation.py b/api/bigrag/routers/evaluation.py
index 9eba0e5e..38b3384e 100644
--- a/api/bigrag/routers/evaluation.py
+++ b/api/bigrag/routers/evaluation.py
@@ -122,7 +122,6 @@ async def run_evaluation(
search_mode=body.search_mode,
filters=case_filters,
reranking_config=get_reranking_config(collection),
- vector_store_provider=collection.get("vector_store_provider"),
)
hit_ids = [r.get("document_id") or r.get("id") for r in outcome.results]
expected = set(case.relevant_ids)
diff --git a/api/bigrag/routers/query.py b/api/bigrag/routers/query.py
index 9ab04c50..811073e7 100644
--- a/api/bigrag/routers/query.py
+++ b/api/bigrag/routers/query.py
@@ -95,7 +95,6 @@ async def query_collection(
search_mode=search_mode,
reranking_config=get_reranking_config(collection),
rerank_override=body.rerank,
- vector_store_provider=collection.get("vector_store_provider"),
)
logger.info(
@@ -214,7 +213,6 @@ async def multi_collection_query(
embedding_models = {}
reranking_configs = {}
- vector_store_providers = {}
resolved_collections = await asyncio.gather(
*[get_collection_or_404(col_name) for col_name in body.collections]
)
@@ -225,7 +223,6 @@ async def multi_collection_query(
except (ImportError, ValueError) as e:
raise HTTPException(status_code=400, detail=f"Collection '{col_name}': {e}") from e
reranking_configs[col_name] = get_reranking_config(collection)
- vector_store_providers[col_name] = collection.get("vector_store_provider") or "qdrant"
include_multimodal_by_collection = {
col_name: bool(body.multimodal and collection.get("multimodal_enabled"))
for col_name, collection in zip(body.collections, resolved_collections, strict=True)
@@ -241,7 +238,6 @@ async def multi_collection_query(
search_mode=body.search_mode,
reranking_configs=reranking_configs,
rerank_override=body.rerank,
- vector_store_providers=vector_store_providers,
)
logger.info("multi-query complete", collections=body.collections, results=len(results))
@@ -314,7 +310,6 @@ async def run_one(item: BatchQueryItem) -> tuple[BatchQueryItem, list[dict], int
search_mode=item.search_mode,
reranking_config=get_reranking_config(collection),
rerank_override=item.rerank,
- vector_store_provider=collection.get("vector_store_provider"),
)
include_multimodal = bool(item.multimodal and collection.get("multimodal_enabled"))
diff --git a/api/bigrag/routers/vectors.py b/api/bigrag/routers/vectors.py
index b2c27e2c..f673299e 100644
--- a/api/bigrag/routers/vectors.py
+++ b/api/bigrag/routers/vectors.py
@@ -90,7 +90,6 @@ async def upsert_vectors(
embeddings=embeddings,
texts=texts,
metadata=metadata,
- provider=collection.get("vector_store_provider"),
)
await invalidate_collection_query_cache(collection_name)
logger.info("vector upsert complete", collection=collection_name, upserted=count)
@@ -123,12 +122,11 @@ async def delete_vectors(
status_code=413,
detail=f"Too many vector IDs. Max: {limits['max_vector_delete_count']}",
)
- collection = await get_collection_or_404(collection_name)
+ await get_collection_or_404(collection_name)
logger.info("vector delete", collection=collection_name, ids=len(body.ids))
await vector_store.delete_by_ids(
collection_name,
body.ids,
- provider=collection.get("vector_store_provider"),
)
await invalidate_collection_query_cache(collection_name)
access_log.set_context(request, metadata={"deleted": len(body.ids)})
diff --git a/api/bigrag/services/backup/exporters.py b/api/bigrag/services/backup/exporters.py
index 4ff39bb2..bcbdbbf1 100644
--- a/api/bigrag/services/backup/exporters.py
+++ b/api/bigrag/services/backup/exporters.py
@@ -77,7 +77,6 @@ async def _export_vector_store(temp_dir: Path) -> dict[str, int]:
async for point in vector_store.iter_collection_points(
collection.name,
with_vectors=False,
- provider=collection.vector_store_provider,
):
f.write(orjson.dumps(_point_payload(point)) + b"\n")
count += 1
@@ -88,7 +87,6 @@ async def _export_vector_store(temp_dir: Path) -> dict[str, int]:
collections_meta.append(
{
"collection": collection.name,
- "provider": collection.vector_store_provider,
"vector_store_collection": collection.name,
"exists": exists,
"points": count,
diff --git a/api/bigrag/services/backup/jobs.py b/api/bigrag/services/backup/jobs.py
index e2332b03..085630ff 100644
--- a/api/bigrag/services/backup/jobs.py
+++ b/api/bigrag/services/backup/jobs.py
@@ -12,7 +12,7 @@
import sqlalchemy as sa
from bigrag.db.engine import session_factory
-from bigrag.db.models import AuditLog, BackupJob, ConnectorSyncJob, VectorMigrationJob
+from bigrag.db.models import AuditLog, BackupJob, ConnectorSyncJob
from bigrag.logging import get_logger
from bigrag.services.maintenance import acquire_backup_lock, active_lock, release_backup_lock
from bigrag.services.queue import ingestion_queue
@@ -25,8 +25,6 @@
logger = get_logger("bigrag.backup")
-ACTIVE_VECTOR_MIGRATION_STATUSES = ("pending", "running", "canceling")
-
async def create_backup_job(*, label: str, created_by: uuid.UUID | None) -> BackupJob:
lock = await active_lock()
@@ -43,17 +41,6 @@ async def create_backup_job(*, label: str, created_by: uuid.UUID | None) -> Back
)
if active is not None:
raise BackupConfigError("A backup is already pending or running")
- active_migration = await session.scalar(
- sa.select(VectorMigrationJob)
- .where(VectorMigrationJob.status.in_(ACTIVE_VECTOR_MIGRATION_STATUSES))
- .order_by(VectorMigrationJob.created_at.desc())
- .limit(1)
- .with_for_update()
- )
- if active_migration is not None:
- raise BackupConfigError(
- "A vector migration is already pending, running, or canceling"
- )
job = BackupJob(label=label.strip(), created_by=created_by)
session.add(job)
await session.refresh(job)
diff --git a/api/bigrag/services/chat/questions/api.py b/api/bigrag/services/chat/questions/api.py
index 03a4bb9a..1fe2cbbf 100644
--- a/api/bigrag/services/chat/questions/api.py
+++ b/api/bigrag/services/chat/questions/api.py
@@ -192,7 +192,6 @@ async def _sample_chunks(
str(document.id),
limit=CHUNK_LIMIT,
offset=offset,
- provider=collection.get("vector_store_provider"),
)
for chunk in chunks:
item = dict(chunk)
diff --git a/api/bigrag/services/chat/turn/prepare.py b/api/bigrag/services/chat/turn/prepare.py
index f8484d2b..617a17fe 100644
--- a/api/bigrag/services/chat/turn/prepare.py
+++ b/api/bigrag/services/chat/turn/prepare.py
@@ -90,7 +90,6 @@ async def _prepare_chat_turn(
search_mode=search_mode,
reranking_config=get_reranking_config(collection),
rerank_override=rerank,
- vector_store_provider=collection.get("vector_store_provider"),
)
sources = await _sources_from_results(session, outcome.results)
timings = ChatTimings(
diff --git a/api/bigrag/services/collection_cache.py b/api/bigrag/services/collection_cache.py
index de44635b..6d13f7d7 100644
--- a/api/bigrag/services/collection_cache.py
+++ b/api/bigrag/services/collection_cache.py
@@ -34,7 +34,6 @@ def _serialize(c: Collection, preset: EmbeddingPreset | None = None) -> dict:
"embedding_preset_id": str(c.embedding_preset_id) if c.embedding_preset_id else None,
"embedding_preset_api_key": (preset.api_key if preset else None) if crypto_ready else None,
"embedding_preset_base_url": preset.base_url if preset else None,
- "vector_store_provider": c.vector_store_provider,
"dimension": c.dimension,
"chunk_size": c.chunk_size,
"chunk_overlap": c.chunk_overlap,
@@ -48,7 +47,6 @@ def _serialize(c: Collection, preset: EmbeddingPreset | None = None) -> dict:
"reranking_api_key": c.reranking_api_key if crypto_ready else None,
"multimodal_enabled": c.multimodal_enabled,
"multimodal_enrichment_enabled": c.multimodal_enrichment_enabled,
- "index_type": c.index_type,
"tenant_field": c.tenant_field,
"metadata_schema": c.metadata_schema,
"metadata": c.meta or {},
diff --git a/api/bigrag/services/collection_provision.py b/api/bigrag/services/collection_provision.py
index 4dca1db4..8afd4683 100644
--- a/api/bigrag/services/collection_provision.py
+++ b/api/bigrag/services/collection_provision.py
@@ -36,21 +36,19 @@ async def verify_embedding_credentials(
) from exc
-def vector_store_unavailable_detail(provider: str) -> str:
- if provider == "turbopuffer":
- return (
- "turbopuffer is not configured. Save a turbopuffer API key in Vector Storage "
- "before creating a turbopuffer collection."
- )
- return f"{provider} vector store is not configured."
+def vector_store_unavailable_detail() -> str:
+ return (
+ "Turbopuffer is not configured. Save a turbopuffer API key in Vector Storage "
+ "before creating a collection."
+ )
-def ensure_vector_store_provider_available(provider: str) -> None:
- if provider in vector_store.configured_providers:
+def ensure_vector_store_available() -> None:
+ if vector_store.provider in vector_store.configured_providers:
return
raise HTTPException(
status_code=400,
- detail=vector_store_unavailable_detail(provider),
+ detail=vector_store_unavailable_detail(),
)
@@ -58,48 +56,38 @@ async def create_vector_store_collection(
body: CreateCollectionRequest,
dimension: int,
) -> None:
- ensure_vector_store_provider_available(body.vector_store_provider)
+ ensure_vector_store_available()
try:
await vector_store.create_collection(
body.name,
dimension,
- index_type=body.index_type,
tenant_field=body.tenant_field,
- provider=body.vector_store_provider,
)
except RuntimeError as e:
message = str(e)
if "API key is not configured" in message or "client is not connected" in message:
raise HTTPException(
status_code=400,
- detail=vector_store_unavailable_detail(body.vector_store_provider),
+ detail=vector_store_unavailable_detail(),
) from e
logger.warning(
"vector collection create failed",
collection=body.name,
- vector_store_provider=body.vector_store_provider,
error_type=e.__class__.__name__,
error=message,
)
raise HTTPException(
status_code=502,
- detail=(
- f"Unable to create {body.vector_store_provider} vector collection. "
- "Check Vector Storage settings."
- ),
+ detail="Unable to create vector collection. Check Vector Storage settings.",
) from e
except httpx.HTTPError as e:
logger.warning(
"vector collection create failed",
collection=body.name,
- vector_store_provider=body.vector_store_provider,
error_type=e.__class__.__name__,
error=str(e),
)
raise HTTPException(
status_code=502,
- detail=(
- f"Unable to create {body.vector_store_provider} vector collection. "
- "Check Vector Storage settings."
- ),
+ detail="Unable to create vector collection. Check Vector Storage settings.",
) from e
diff --git a/api/bigrag/services/collections.py b/api/bigrag/services/collections.py
index cf7d72d5..a6a7533a 100644
--- a/api/bigrag/services/collections.py
+++ b/api/bigrag/services/collections.py
@@ -25,14 +25,13 @@ async def delete_collection(session: AsyncSession, name: str) -> str:
logger.info("delete collection jobs cancelled", collection=name, flushed=flushed)
deleted_id = str(collection.id)
- vector_store_provider = collection.vector_store_provider
await session.delete(collection)
await session.commit()
await collection_cache.invalidate(name)
await invalidate_collection_query_cache(name)
logger.info("delete collection database records removed", collection=name)
- await vector_store.delete_collection(name, provider=vector_store_provider)
+ await vector_store.delete_collection(name)
logger.info("delete collection vectors dropped", collection=name)
deleted = await get_storage().delete_prefix(f"{name}/")
@@ -51,7 +50,6 @@ async def truncate_collection(session: AsyncSession, name: str) -> str:
logger.info("truncate collection jobs cancelled", collection=name, flushed=flushed)
collection_id = str(collection.id)
- vector_store_provider = collection.vector_store_provider
await session.execute(sa.delete(Document).where(Document.collection_id == collection.id))
await session.execute(
sa.update(Collection).where(Collection.id == collection.id).values(document_count=0)
@@ -61,7 +59,7 @@ async def truncate_collection(session: AsyncSession, name: str) -> str:
await invalidate_collection_query_cache(name)
logger.info("truncate collection documents removed", collection=name)
- await vector_store.delete_collection(name, provider=vector_store_provider)
+ await vector_store.delete_collection(name)
logger.info("truncate collection vectors cleared", collection=name)
deleted = await get_storage().delete_prefix(f"{name}/")
diff --git a/api/bigrag/services/connectors/documents.py b/api/bigrag/services/connectors/documents.py
index 19844259..72c5e9f0 100644
--- a/api/bigrag/services/connectors/documents.py
+++ b/api/bigrag/services/connectors/documents.py
@@ -104,7 +104,6 @@ async def _put_downloaded(storage_key: str) -> None:
await vector_store.delete_by_document(
source.collection_name,
str(doc.id),
- provider=collection.vector_store_provider,
)
old_path = doc.file_path
storage_key = f"{source.collection_name}/{doc.id}{downloaded.file_ext}"
@@ -156,7 +155,6 @@ async def delete_synced_document(
await vector_store.delete_by_document(
source.collection_name,
str(doc.id),
- provider=collection.vector_store_provider,
)
storage = get_storage()
await storage.delete(doc.file_path)
diff --git a/api/bigrag/services/connectors/manifest.py b/api/bigrag/services/connectors/manifest.py
index e4358686..6b869b59 100644
--- a/api/bigrag/services/connectors/manifest.py
+++ b/api/bigrag/services/connectors/manifest.py
@@ -26,7 +26,6 @@ def collection_dict_for_sync(collection: Collection) -> dict[str, Any]:
"chunk_size": collection.chunk_size,
"chunk_overlap": collection.chunk_overlap,
"chunk_strategy": collection.chunk_strategy or "paragraph",
- "vector_store_provider": collection.vector_store_provider,
"tenant_field": collection.tenant_field,
"metadata_schema": collection.metadata_schema,
}
diff --git a/api/bigrag/services/health.py b/api/bigrag/services/health.py
index cacd6952..89aa6232 100644
--- a/api/bigrag/services/health.py
+++ b/api/bigrag/services/health.py
@@ -211,11 +211,6 @@ async def _check_redis():
healthy = False
else:
checks[name] = True
- checks["vector_store_provider"] = "per_collection"
- checks["qdrant"] = (
- checks["vector_store"] if "qdrant" in getattr(vs, "configured_providers", ()) else None
- )
-
embedding_result = await check_embedding_provider()
checks.update(embedding_result)
if not embedding_result.get("embedding"):
diff --git a/api/bigrag/services/ingestion_job.py b/api/bigrag/services/ingestion_job.py
index 3bad2281..00e928d3 100644
--- a/api/bigrag/services/ingestion_job.py
+++ b/api/bigrag/services/ingestion_job.py
@@ -17,7 +17,6 @@ class IngestionJob:
chunk_size: int
chunk_overlap: int
chunk_strategy: str = "paragraph"
- vector_store_provider: str = "qdrant"
tenant_field: str | None = None
embedding_base_url: str | None = None
multimodal_enabled: bool = False
@@ -45,7 +44,6 @@ def serialize(self) -> bytes:
"chunk_size": self.chunk_size,
"chunk_overlap": self.chunk_overlap,
"chunk_strategy": self.chunk_strategy,
- "vector_store_provider": self.vector_store_provider,
"tenant_field": self.tenant_field,
"attempt": self.attempt,
"max_attempts": self.max_attempts,
@@ -79,7 +77,6 @@ def create_ingestion_job(
chunk_size=collection["chunk_size"],
chunk_overlap=collection["chunk_overlap"],
chunk_strategy=collection.get("chunk_strategy") or "paragraph",
- vector_store_provider=collection.get("vector_store_provider") or "qdrant",
tenant_field=collection.get("tenant_field"),
multimodal_enabled=bool(collection.get("multimodal_enabled")),
multimodal_enrichment_enabled=bool(collection.get("multimodal_enrichment_enabled")),
diff --git a/api/bigrag/services/jobs/actors.py b/api/bigrag/services/jobs/actors.py
index c4dfd69b..f24204ad 100644
--- a/api/bigrag/services/jobs/actors.py
+++ b/api/bigrag/services/jobs/actors.py
@@ -71,10 +71,6 @@ def enqueue_backup_job(job_id: str) -> None:
run_backup.send(job_id)
-def enqueue_vector_migration_job(job_id: str) -> None:
- run_vector_migration.send(job_id)
-
-
def seed_periodic_jobs(enabled_queues: set[str] | None = None) -> None:
if enabled_queues is None or MAINTENANCE_QUEUE in enabled_queues:
_schedule_sync(
@@ -227,18 +223,6 @@ async def _run_backup(job_id: str) -> None:
await run_backup_job(job_id)
-@dramatiq.actor(queue_name=MAINTENANCE_QUEUE, max_retries=0, broker=broker)
-def run_vector_migration(job_id: str) -> None:
- _run(_run_vector_migration, job_id)
-
-
-async def _run_vector_migration(job_id: str) -> None:
- await ensure_worker_runtime()
- from bigrag.services.vector_migration import run_vector_migration_job
-
- await run_vector_migration_job(job_id)
-
-
@dramatiq.actor(queue_name=MAINTENANCE_QUEUE, max_retries=0, broker=broker)
def run_cleanup() -> None:
try:
diff --git a/api/bigrag/services/jobs/runtime.py b/api/bigrag/services/jobs/runtime.py
index 6fab5c0a..4214654d 100644
--- a/api/bigrag/services/jobs/runtime.py
+++ b/api/bigrag/services/jobs/runtime.py
@@ -64,21 +64,16 @@ async def ensure_worker_runtime() -> None:
runtime = await runtime_settings.get_values(
[
"ingestion_workers",
- "qdrant_connect_timeout_seconds",
- "qdrant_required",
- "qdrant_search_ef",
- "qdrant_url",
"turbopuffer_api_key",
+ "turbopuffer_base_url",
"turbopuffer_namespace_prefix",
"turbopuffer_region",
]
)
logger.info("worker runtime settings loaded")
vector_store.configure(
- qdrant_url=runtime["qdrant_url"],
- connect_timeout_seconds=runtime["qdrant_connect_timeout_seconds"],
- search_ef=runtime["qdrant_search_ef"],
turbopuffer_api_key=runtime["turbopuffer_api_key"],
+ turbopuffer_base_url=runtime["turbopuffer_base_url"],
turbopuffer_region=runtime["turbopuffer_region"],
turbopuffer_namespace_prefix=runtime["turbopuffer_namespace_prefix"],
)
diff --git a/api/bigrag/services/queue_embedding/insert.py b/api/bigrag/services/queue_embedding/insert.py
index 82242a67..8479caab 100644
--- a/api/bigrag/services/queue_embedding/insert.py
+++ b/api/bigrag/services/queue_embedding/insert.py
@@ -107,7 +107,6 @@ async def chunk_and_embed(
job.collection_name,
job.embedding_dimension,
tenant_field=getattr(job, "tenant_field", None),
- provider=job.vector_store_provider,
)
await ensure_job_current(job)
@@ -246,7 +245,6 @@ async def _embed_one_bounded(bn, bs, be, bc):
texts=batch_texts,
embeddings=embeddings,
metadata=metadata,
- provider=job.vector_store_provider,
)
try:
await ensure_job_current(job)
@@ -254,7 +252,6 @@ async def _embed_one_bounded(bn, bs, be, bc):
await vector_store.delete_by_document(
job.collection_name,
doc,
- provider=job.vector_store_provider,
)
raise
insert_elapsed = time.monotonic() - t1
@@ -266,7 +263,6 @@ async def _embed_one_bounded(bn, bs, be, bc):
await vector_store.delete_by_ids(
job.collection_name,
[f"{doc}_{i}" for i in range(batch_start, batch_end)],
- provider=job.vector_store_provider,
)
except Exception as cleanup_exc:
logger.warning(
diff --git a/api/bigrag/services/runtime_setting_specs/vector_store.py b/api/bigrag/services/runtime_setting_specs/vector_store.py
index 1080ec7f..c7f51aa4 100644
--- a/api/bigrag/services/runtime_setting_specs/vector_store.py
+++ b/api/bigrag/services/runtime_setting_specs/vector_store.py
@@ -3,32 +3,6 @@
from bigrag.services.runtime_setting_specs._spec import SettingSpec
VECTOR_STORE_SPECS: tuple[SettingSpec, ...] = (
- SettingSpec(
- key="qdrant_url",
- group="vector_store",
- label="Qdrant URL",
- kind="string",
- default="http://localhost:6333",
- description="Qdrant connection URL.",
- ),
- SettingSpec(
- key="qdrant_connect_timeout_seconds",
- group="vector_store",
- label="Qdrant connect timeout",
- kind="int",
- default=10,
- description="Qdrant startup connection timeout in seconds.",
- min=0,
- max=300,
- ),
- SettingSpec(
- key="qdrant_required",
- group="vector_store",
- label="Require vector store",
- kind="bool",
- default=False,
- description="Fail startup if configured vector-store clients cannot be reached.",
- ),
SettingSpec(
key="turbopuffer_api_key",
group="vector_store",
@@ -38,6 +12,14 @@
description="turbopuffer API key.",
secret=True,
),
+ SettingSpec(
+ key="turbopuffer_base_url",
+ group="vector_store",
+ label="turbopuffer base URL",
+ kind="string",
+ default=None,
+ description="Optional turbopuffer API base URL.",
+ ),
SettingSpec(
key="turbopuffer_region",
group="vector_store",
@@ -54,14 +36,4 @@
default="bigrag_",
description="Prefix prepended to turbopuffer namespace names.",
),
- SettingSpec(
- key="qdrant_search_ef",
- group="vector_store",
- label="Qdrant search ef",
- kind="int",
- default=None,
- description="Optional Qdrant HNSW search ef override.",
- min=1,
- max=10000,
- ),
)
diff --git a/api/bigrag/services/runtime_settings_apply.py b/api/bigrag/services/runtime_settings_apply.py
index 711d5549..f9784f4c 100644
--- a/api/bigrag/services/runtime_settings_apply.py
+++ b/api/bigrag/services/runtime_settings_apply.py
@@ -42,10 +42,8 @@
}
VECTOR_CONFIG_KEYS = {
- "qdrant_connect_timeout_seconds",
- "qdrant_search_ef",
- "qdrant_url",
"turbopuffer_api_key",
+ "turbopuffer_base_url",
"turbopuffer_namespace_prefix",
"turbopuffer_region",
}
@@ -155,8 +153,9 @@ async def _prepare_vector_backend(values: dict[str, Any]) -> VectorStore:
store = VectorStore()
_configure_vector_store(store, values)
try:
- store.connect()
- await store.health_check()
+ if values.get("turbopuffer_api_key"):
+ store.connect()
+ await store.health_check()
return store
except Exception:
await store.close()
@@ -165,10 +164,8 @@ async def _prepare_vector_backend(values: dict[str, Any]) -> VectorStore:
def _configure_vector_store(store: VectorStore, values: dict[str, Any]) -> None:
store.configure(
- qdrant_url=values["qdrant_url"],
- connect_timeout_seconds=values["qdrant_connect_timeout_seconds"],
- search_ef=values["qdrant_search_ef"],
turbopuffer_api_key=values["turbopuffer_api_key"],
+ turbopuffer_base_url=values["turbopuffer_base_url"],
turbopuffer_region=values["turbopuffer_region"],
turbopuffer_namespace_prefix=values["turbopuffer_namespace_prefix"],
)
diff --git a/api/bigrag/services/vector_migration/__init__.py b/api/bigrag/services/vector_migration/__init__.py
deleted file mode 100644
index 28c5b5a8..00000000
--- a/api/bigrag/services/vector_migration/__init__.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from __future__ import annotations
-
-from bigrag.services.vector_migration.jobs import (
- VectorMigrationConflictError,
- VectorMigrationError,
- create_vector_migration_job,
- delete_vector_migration_job,
- run_vector_migration_job,
-)
-
-__all__ = [
- "VectorMigrationConflictError",
- "VectorMigrationError",
- "create_vector_migration_job",
- "delete_vector_migration_job",
- "run_vector_migration_job",
-]
diff --git a/api/bigrag/services/vector_migration/jobs.py b/api/bigrag/services/vector_migration/jobs.py
deleted file mode 100644
index b4cf092c..00000000
--- a/api/bigrag/services/vector_migration/jobs.py
+++ /dev/null
@@ -1,522 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-import uuid
-from datetime import UTC, datetime
-from typing import Any
-
-import sqlalchemy as sa
-
-from bigrag.db.engine import session_factory
-from bigrag.db.models import AuditLog, BackupJob, Collection, ConnectorSyncJob, VectorMigrationJob
-from bigrag.logging import get_logger
-from bigrag.services import collection_cache
-from bigrag.services.error_sanitize import sanitize_message_text
-from bigrag.services.maintenance import (
- acquire_maintenance_lock,
- active_lock,
- release_maintenance_lock,
-)
-from bigrag.services.queue import ingestion_queue
-from bigrag.services.retrieval import invalidate_collection_query_cache
-from bigrag.services.runtime_settings import get_value
-from bigrag.services.vector_store import vector_store
-from bigrag.services.vector_store._util import validate_provider
-from bigrag.services.vector_store.base import _FIXED_PAYLOAD_FIELDS
-
-logger = get_logger("bigrag.vector_migration")
-
-ACTIVE_STATUSES = ("pending", "running", "canceling")
-ACTIVE_BACKUP_STATUSES = ("pending", "running")
-
-
-class VectorMigrationError(RuntimeError):
- pass
-
-
-class VectorMigrationConflictError(VectorMigrationError):
- pass
-
-
-class VectorMigrationCanceledError(VectorMigrationError):
- pass
-
-
-async def create_vector_migration_job(
- *,
- collection_name: str,
- target_provider: str,
- created_by: uuid.UUID | None,
-) -> VectorMigrationJob:
- target = validate_provider(target_provider)
- lock = await active_lock()
- if lock is not None:
- raise VectorMigrationConflictError(f"Instance maintenance active: {lock.reason}")
- if target not in vector_store.configured_providers:
- raise VectorMigrationError(f"{target} vector store is not configured")
- async with session_factory()() as session:
- async with session.begin():
- active_backup = await session.scalar(
- sa.select(BackupJob)
- .where(BackupJob.status.in_(ACTIVE_BACKUP_STATUSES))
- .order_by(BackupJob.created_at.desc())
- .limit(1)
- .with_for_update()
- )
- if active_backup is not None:
- raise VectorMigrationConflictError("A backup is already pending or running")
- active = await session.scalar(
- sa.select(VectorMigrationJob)
- .where(VectorMigrationJob.status.in_(ACTIVE_STATUSES))
- .order_by(VectorMigrationJob.created_at.desc())
- .limit(1)
- .with_for_update()
- )
- if active is not None:
- raise VectorMigrationConflictError(
- "A vector migration is already pending, running, or canceling"
- )
- collection = await session.scalar(
- sa.select(Collection).where(Collection.name == collection_name).with_for_update()
- )
- if collection is None:
- raise VectorMigrationError("Collection not found")
- source = validate_provider(collection.vector_store_provider)
- if source == target:
- raise VectorMigrationConflictError("Collection already uses that vector provider")
- job = VectorMigrationJob(
- collection_id=collection.id,
- collection_name=collection.name,
- source_provider=source,
- target_provider=target,
- created_by=created_by,
- )
- session.add(job)
- await session.refresh(job)
- return job
-
-
-async def delete_vector_migration_job(job_id: uuid.UUID) -> str | None:
- async with session_factory()() as session:
- async with session.begin():
- job = await session.scalar(
- sa.select(VectorMigrationJob)
- .where(VectorMigrationJob.id == job_id)
- .with_for_update()
- )
- if job is None:
- return None
- if job.status == "pending" or job.status in {"succeeded", "failed"}:
- await session.delete(job)
- return "deleted"
- details = dict(job.details or {})
- details["delete_requested"] = True
- job.details = details
- job.status = "canceling"
- job.phase = "canceling"
- job.updated_at = datetime.now(UTC)
- return "stop_requested"
-
-
-async def run_vector_migration_job(job_id: str) -> None:
- owner_id = uuid.UUID(job_id)
- locked = False
- try:
- job = await _get_job(owner_id)
- if job is None:
- return
- if job.status != "pending":
- return
- locked = await acquire_maintenance_lock(
- owner_id,
- reason=f"vector migration for {job.collection_name}",
- metadata={
- "collection": job.collection_name,
- "source_provider": job.source_provider,
- "target_provider": job.target_provider,
- },
- )
- if not locked:
- await _fail_job(owner_id, "Another maintenance lock is active")
- return
- if not await _mark_running(owner_id):
- return
- await _raise_if_delete_requested(owner_id)
- await _wait_for_connector_sync_drain(owner_id)
- await _wait_for_ingestion_drain(owner_id)
- await _run_locked_migration(owner_id)
- except VectorMigrationCanceledError:
- await _delete_job(owner_id, "vector_migration.deleted", {"reason": "canceled"})
- except Exception as exc:
- logger.exception("vector migration failed", job_id=job_id, error=str(exc))
- await _fail_job(owner_id, sanitize_message_text(str(exc)) or "Vector migration failed")
- finally:
- if locked:
- await release_maintenance_lock(owner_id)
-
-
-async def _run_locked_migration(job_id: uuid.UUID) -> None:
- job, collection = await _load_job_and_collection(job_id)
- if job is None:
- return
- if collection is None:
- await _fail_job(job_id, "Collection not found")
- return
- source = validate_provider(job.source_provider)
- target = validate_provider(job.target_provider)
- cutover_done = False
- copied = 0
- try:
- if collection.vector_store_provider != source:
- raise VectorMigrationError(
- "Collection vector provider changed before migration started"
- )
- await _raise_if_delete_requested(job_id)
- await _update_job(job_id, phase="provisioning", progress=0.2)
- await vector_store.delete_collection(collection.name, provider=target)
- await _raise_if_delete_requested(job_id)
- await vector_store.create_collection(
- collection.name,
- collection.dimension,
- index_type=collection.index_type,
- tenant_field=collection.tenant_field,
- provider=target,
- )
- await _raise_if_delete_requested(job_id)
- copied = await _copy_points(job_id, collection.name, source, target)
- await _raise_if_delete_requested(job_id)
- await _update_job(
- job_id,
- phase="verifying",
- progress=0.86,
- copied_points=copied,
- total_points=copied,
- )
- target_count = 0 if copied == 0 else await _count_points(collection.name, target)
- if target_count != copied:
- raise VectorMigrationError(
- f"Target point count mismatch: copied {copied}, target has {target_count}"
- )
- await _raise_if_delete_requested(job_id)
- await _cutover_collection(job_id, collection.id, collection.name, source, target)
- cutover_done = True
- await _update_job(
- job_id,
- phase="cleanup",
- progress=0.94,
- copied_points=copied,
- total_points=copied,
- )
- await vector_store.delete_collection(collection.name, provider=source)
- await _complete_job(job_id, copied)
- except VectorMigrationCanceledError:
- if not cutover_done:
- try:
- await vector_store.delete_collection(collection.name, provider=target)
- except Exception as cleanup_exc:
- logger.warning(
- "canceled vector migration cleanup failed",
- collection=collection.name,
- target_provider=target,
- error=str(cleanup_exc),
- )
- await _delete_job(job_id, "vector_migration.deleted", {"reason": "canceled"})
- except Exception as exc:
- message = sanitize_message_text(str(exc)) or "Vector migration failed"
- if not cutover_done:
- try:
- await vector_store.delete_collection(collection.name, provider=target)
- except Exception as cleanup_exc:
- logger.warning(
- "partial vector migration cleanup failed",
- collection=collection.name,
- target_provider=target,
- error=str(cleanup_exc),
- )
- await _fail_job(
- job_id,
- message,
- phase="cleanup_failed" if cutover_done else "failed",
- copied_points=copied,
- total_points=copied or None,
- )
-
-
-async def _copy_points(
- job_id: uuid.UUID,
- collection: str,
- source: str,
- target: str,
-) -> int:
- batch_size = max(1, min(int(await get_value("ingestion_batch_size")), 1000))
- copied = 0
- batch: list[dict[str, Any]] = []
- await _raise_if_delete_requested(job_id)
- await _update_job(job_id, phase="copying", progress=0.32)
- async for point in vector_store.iter_collection_points(
- collection,
- with_vectors=True,
- provider=source,
- ):
- await _raise_if_delete_requested(job_id)
- batch.append(_normalise_point(point))
- if len(batch) >= batch_size:
- copied += await _insert_batch(collection, target, batch)
- batch.clear()
- await _update_job(
- job_id,
- copied_points=copied,
- progress=min(0.84, 0.34 + copied / (copied + batch_size) * 0.48),
- )
- if batch:
- await _raise_if_delete_requested(job_id)
- copied += await _insert_batch(collection, target, batch)
- await _update_job(job_id, copied_points=copied, progress=0.84)
- return copied
-
-
-def _normalise_point(point: dict[str, Any]) -> dict[str, Any]:
- payload = dict(point.get("payload") or {})
- vector = point.get("vector")
- if vector is None:
- raise VectorMigrationError("Source point is missing its vector")
- public_id = str(payload.get("id") or point.get("id") or "")
- if not public_id:
- raise VectorMigrationError("Source point is missing its id")
- return {
- "id": public_id,
- "document_id": str(payload.get("document_id") or ""),
- "chunk_index": int(payload.get("chunk_index") or 0),
- "text": str(payload.get("text") or ""),
- "vector": vector,
- "metadata": {
- k: v for k, v in payload.items() if k not in _FIXED_PAYLOAD_FIELDS and v is not None
- },
- }
-
-
-async def _insert_batch(
- collection: str,
- target: str,
- batch: list[dict[str, Any]],
-) -> int:
- return await vector_store.insert(
- collection=collection,
- ids=[item["id"] for item in batch],
- document_ids=[item["document_id"] for item in batch],
- chunk_indices=[item["chunk_index"] for item in batch],
- texts=[item["text"] for item in batch],
- embeddings=[item["vector"] for item in batch],
- metadata=[item["metadata"] for item in batch],
- provider=target,
- )
-
-
-async def _count_points(collection: str, provider: str) -> int:
- count = 0
- async for _point in vector_store.iter_collection_points(
- collection,
- with_vectors=False,
- provider=provider,
- ):
- count += 1
- return count
-
-
-async def _cutover_collection(
- job_id: uuid.UUID,
- collection_id: uuid.UUID,
- collection_name: str,
- source: str,
- target: str,
-) -> None:
- await _update_job(job_id, phase="cutover", progress=0.9)
- async with session_factory()() as session:
- async with session.begin():
- collection = await session.scalar(
- sa.select(Collection).where(Collection.id == collection_id).with_for_update()
- )
- if collection is None:
- raise VectorMigrationError("Collection not found during cutover")
- if collection.vector_store_provider != source:
- raise VectorMigrationError("Collection vector provider changed during migration")
- collection.vector_store_provider = target
- await collection_cache.invalidate(collection_name)
- await invalidate_collection_query_cache(collection_name)
-
-
-async def _wait_for_ingestion_drain(job_id: uuid.UUID, max_wait_seconds: int = 1800) -> None:
- deadline = asyncio.get_event_loop().time() + max_wait_seconds
- while True:
- await _raise_if_delete_requested(job_id)
- stats = await ingestion_queue.stats
- processing = int(stats.get("processing") or 0)
- if processing <= 0:
- return
- if asyncio.get_event_loop().time() >= deadline:
- raise VectorMigrationError(
- f"Timed out waiting for ingestion drain after {max_wait_seconds}s"
- )
- await _update_job(job_id, phase="draining", progress=0.12)
- await asyncio.sleep(1)
-
-
-async def _wait_for_connector_sync_drain(job_id: uuid.UUID, max_wait_seconds: int = 1800) -> None:
- deadline = asyncio.get_event_loop().time() + max_wait_seconds
- while True:
- await _raise_if_delete_requested(job_id)
- async with session_factory()() as session:
- running = await session.scalar(
- sa.select(sa.func.count())
- .select_from(ConnectorSyncJob)
- .where(ConnectorSyncJob.status == "running")
- )
- if int(running or 0) <= 0:
- return
- if asyncio.get_event_loop().time() >= deadline:
- raise VectorMigrationError(
- f"Timed out waiting for connector sync drain after {max_wait_seconds}s"
- )
- await _update_job(job_id, phase="draining", progress=0.08)
- await asyncio.sleep(1)
-
-
-async def _load_job_and_collection(
- job_id: uuid.UUID,
-) -> tuple[VectorMigrationJob | None, Collection | None]:
- async with session_factory()() as session:
- job = await session.get(VectorMigrationJob, job_id)
- if job is None:
- return None, None
- collection = await session.scalar(
- sa.select(Collection).where(Collection.name == job.collection_name)
- )
- return job, collection
-
-
-async def _get_job(job_id: uuid.UUID) -> VectorMigrationJob | None:
- async with session_factory()() as session:
- return await session.get(VectorMigrationJob, job_id)
-
-
-async def _mark_running(job_id: uuid.UUID) -> bool:
- updated = await _update_job(
- job_id,
- status="running",
- phase="draining",
- progress=0.04,
- started_at=datetime.now(UTC),
- )
- if updated <= 0:
- return False
- await _insert_audit(job_id, "vector_migration.start", {})
- return True
-
-
-async def _complete_job(job_id: uuid.UUID, copied_points: int) -> None:
- await _update_job(
- job_id,
- status="succeeded",
- phase="complete",
- progress=1.0,
- copied_points=copied_points,
- total_points=copied_points,
- completed_at=datetime.now(UTC),
- )
- await _insert_audit(job_id, "vector_migration.succeeded", {"copied_points": copied_points})
- if await _delete_requested(job_id):
- await _delete_job(
- job_id,
- "vector_migration.deleted",
- {"reason": "completed_after_delete_request"},
- )
-
-
-async def _fail_job(
- job_id: uuid.UUID,
- message: str,
- *,
- phase: str = "failed",
- copied_points: int | None = None,
- total_points: int | None = None,
-) -> None:
- values: dict[str, Any] = {
- "status": "failed",
- "phase": phase,
- "error_message": sanitize_message_text(message),
- "completed_at": datetime.now(UTC),
- }
- if copied_points is not None:
- values["copied_points"] = copied_points
- if total_points is not None:
- values["total_points"] = total_points
- await _update_job(job_id, **values)
- await _insert_audit(job_id, "vector_migration.failed", {"error": values["error_message"]})
- if await _delete_requested(job_id):
- await _delete_job(
- job_id,
- "vector_migration.deleted",
- {"reason": "failed_after_delete_request"},
- )
-
-
-async def _update_job(job_id: uuid.UUID, **values: Any) -> int:
- async with session_factory()() as session:
- values["updated_at"] = sa.func.now()
- result = await session.execute(
- sa.update(VectorMigrationJob).where(VectorMigrationJob.id == job_id).values(**values)
- )
- await session.commit()
- return result.rowcount or 0
-
-
-async def _insert_audit(job_id: uuid.UUID, action: str, metadata: dict[str, Any]) -> None:
- async with session_factory()() as session:
- job = await session.get(VectorMigrationJob, job_id)
- session.add(
- AuditLog(
- actor_id=job.created_by if job else None,
- actor_email=None,
- api_key_id=None,
- action=action,
- resource_type="vector_migration_job",
- resource_id=str(job_id),
- meta=metadata,
- ip=None,
- user_agent=None,
- )
- )
- await session.commit()
-
-
-async def _delete_requested(job_id: uuid.UUID) -> bool:
- job = await _get_job(job_id)
- if job is None:
- return False
- return job.status == "canceling" or bool((job.details or {}).get("delete_requested"))
-
-
-async def _raise_if_delete_requested(job_id: uuid.UUID) -> None:
- if await _delete_requested(job_id):
- raise VectorMigrationCanceledError("Vector migration deletion requested")
-
-
-async def _delete_job(job_id: uuid.UUID, action: str, metadata: dict[str, Any]) -> None:
- async with session_factory()() as session:
- job = await session.get(VectorMigrationJob, job_id)
- session.add(
- AuditLog(
- actor_id=job.created_by if job else None,
- actor_email=None,
- api_key_id=None,
- action=action,
- resource_type="vector_migration_job",
- resource_id=str(job_id),
- meta=metadata,
- ip=None,
- user_agent=None,
- )
- )
- if job is not None:
- await session.delete(job)
- await session.commit()
diff --git a/api/bigrag/services/vector_store/_util.py b/api/bigrag/services/vector_store/_util.py
index 4b847419..20c39cf4 100644
--- a/api/bigrag/services/vector_store/_util.py
+++ b/api/bigrag/services/vector_store/_util.py
@@ -7,7 +7,7 @@
logger = get_logger("bigrag.vector_store")
-PROVIDERS: tuple[VectorStoreProvider, ...] = ("qdrant", "turbopuffer")
+PROVIDERS: tuple[VectorStoreProvider, ...] = ("turbopuffer",)
def validate_provider(value: str) -> VectorStoreProvider:
diff --git a/api/bigrag/services/vector_store/base.py b/api/bigrag/services/vector_store/base.py
index 6992ef0f..d59f98de 100644
--- a/api/bigrag/services/vector_store/base.py
+++ b/api/bigrag/services/vector_store/base.py
@@ -6,7 +6,7 @@
from bigrag.services._retrieval_filters import FilterExpression
-VectorStoreProvider = Literal["qdrant", "turbopuffer"]
+VectorStoreProvider = Literal["turbopuffer"]
_POINT_NAMESPACE = uuid.UUID("1b04f7ca-0c3b-5d76-a5bb-6e4b4a40f61d")
_FIXED_PAYLOAD_FIELDS = {"id", "text", "document_id", "chunk_index", "embedding"}
@@ -29,7 +29,6 @@ class VectorStoreFeatureError(RuntimeError):
class VectorStoreBackend(Protocol):
provider: VectorStoreProvider
- supports_text_search: bool
def connect(self) -> None: ...
diff --git a/api/bigrag/services/vector_store/facade.py b/api/bigrag/services/vector_store/facade.py
index 956a9139..b02daed4 100644
--- a/api/bigrag/services/vector_store/facade.py
+++ b/api/bigrag/services/vector_store/facade.py
@@ -6,24 +6,20 @@
from typing import Any
from bigrag.config import settings as _app_settings
-from bigrag.logging import get_logger
from bigrag.services._retrieval_filters import FilterExpression
-from bigrag.services.error_sanitize import sanitize_message_text
-from bigrag.services.vector_store._util import PROVIDERS, close_backends, validate_provider
-from bigrag.services.vector_store.base import VectorStoreBackend, VectorStoreProvider
-from bigrag.services.vector_store.qdrant import QdrantVectorStore
+from bigrag.services.vector_store._util import close_backends
+from bigrag.services.vector_store.base import VectorStoreBackend
from bigrag.services.vector_store.turbopuffer import TurbopufferVectorStore
-logger = get_logger("bigrag.vector_store")
-
class VectorStore:
def __init__(self) -> None:
- self.provider: str = "collection"
- self.backends: dict[VectorStoreProvider, VectorStoreBackend] = {}
- self._configured_providers: set[VectorStoreProvider] = {"qdrant"}
- self._fallback_provider: VectorStoreProvider = "qdrant"
- self.backend = QdrantVectorStore()
+ self._backend_instance: VectorStoreBackend = TurbopufferVectorStore(
+ api_key=_app_settings.turbopuffer_api_key,
+ region=_app_settings.turbopuffer_region,
+ namespace_prefix=_app_settings.turbopuffer_namespace_prefix,
+ base_url=_app_settings.turbopuffer_base_url,
+ )
self.client: Any | None = None
self._condition = asyncio.Condition()
self._active = 0
@@ -31,73 +27,32 @@ def __init__(self) -> None:
@property
def backend(self) -> VectorStoreBackend:
- return self.backends[self._fallback_provider]
+ return self._backend_instance
@backend.setter
def backend(self, value: VectorStoreBackend) -> None:
- provider = validate_provider(getattr(value, "provider", self._fallback_provider))
- self.backends[provider] = value
- self._fallback_provider = provider
- self._configured_providers.add(provider)
+ self._backend_instance = value
self._sync_client()
def configure(
self,
url: str | None = None,
*,
- provider: VectorStoreProvider | None = None,
- connect_timeout_seconds: int | float | None = 10,
- search_ef: int | None = None,
- qdrant_url: str | None = None,
- qdrant_prefer_grpc: bool | None = None,
- qdrant_grpc_port: int | None = None,
turbopuffer_api_key: str | None = None,
turbopuffer_region: str = "aws-us-east-1",
turbopuffer_namespace_prefix: str = "bigrag_",
+ turbopuffer_base_url: str | None = None,
+ **_: Any,
) -> None:
- if provider is not None:
- validate_provider(provider)
- prefer_grpc = (
- qdrant_prefer_grpc
- if qdrant_prefer_grpc is not None
- else _app_settings.qdrant_prefer_grpc
+ self.backend = TurbopufferVectorStore(
+ api_key=turbopuffer_api_key,
+ region=turbopuffer_region or "aws-us-east-1",
+ namespace_prefix=turbopuffer_namespace_prefix,
+ base_url=turbopuffer_base_url,
)
- grpc_port = (
- qdrant_grpc_port if qdrant_grpc_port is not None else _app_settings.qdrant_grpc_port
- )
- self.backends = {
- "qdrant": QdrantVectorStore(
- qdrant_url or url or "http://localhost:6333",
- connect_timeout_seconds=connect_timeout_seconds,
- search_ef=search_ef,
- prefer_grpc=prefer_grpc,
- grpc_port=grpc_port,
- ),
- "turbopuffer": TurbopufferVectorStore(
- api_key=turbopuffer_api_key,
- region=turbopuffer_region,
- namespace_prefix=turbopuffer_namespace_prefix,
- ),
- }
- self._configured_providers = {"qdrant"}
- if turbopuffer_api_key or provider == "turbopuffer":
- self._configured_providers.add("turbopuffer")
- self._fallback_provider = provider or "qdrant"
- self.provider = provider or "collection"
- self._sync_client()
-
- def supports_text_search_for(self, provider: VectorStoreProvider | None = None) -> bool:
- return self.backends[
- validate_provider(provider or self._fallback_provider)
- ].supports_text_search
-
- @property
- def configured_providers(self) -> tuple[VectorStoreProvider, ...]:
- return tuple(provider for provider in PROVIDERS if provider in self._configured_providers)
def connect(self) -> None:
- for provider in self.configured_providers:
- self.backends[provider].connect()
+ self.backend.connect()
self._sync_client()
async def close(self) -> None:
@@ -106,7 +61,7 @@ async def close(self) -> None:
try:
while self._active:
await self._condition.wait()
- await close_backends(self.backends)
+ await self.backend.close()
self._sync_client()
finally:
self._swapping = False
@@ -117,23 +72,20 @@ async def replace_with(self, other: VectorStore) -> None:
self._swapping = True
while self._active:
await self._condition.wait()
- old_backends = dict(self.backends)
- self.provider = other.provider
- self.backends = dict(other.backends)
- self._configured_providers = set(other._configured_providers)
- self._fallback_provider = other._fallback_provider
+ old_backend = self.backend
+ self.backend = other.backend
self._sync_client()
- await close_backends(old_backends, log_errors=True)
+ await close_backends({"turbopuffer": old_backend}, log_errors=True)
self._swapping = False
self._condition.notify_all()
@asynccontextmanager
- async def _backend(self, provider: VectorStoreProvider) -> AsyncIterator[VectorStoreBackend]:
+ async def _backend(self) -> AsyncIterator[VectorStoreBackend]:
async with self._condition:
while self._swapping:
await self._condition.wait()
self._active += 1
- backend = self.backends[provider]
+ backend = self.backend
try:
yield backend
finally:
@@ -143,82 +95,18 @@ async def _backend(self, provider: VectorStoreProvider) -> AsyncIterator[VectorS
self._condition.notify_all()
async def health_check(self) -> None:
- errors: list[str] = []
- for provider in self.configured_providers:
- try:
- async with self._backend(provider) as backend:
- await backend.health_check()
- except Exception as exc:
- logger.warning(
- "vector store provider unhealthy",
- provider=provider,
- error_type=type(exc).__name__,
- )
- errors.append(f"{provider}: {type(exc).__name__}")
- if errors:
- raise RuntimeError("; ".join(errors))
+ async with self._backend() as backend:
+ await backend.health_check()
- async def provider_health(self) -> dict[str, dict[str, object]]:
- results: dict[str, dict[str, object]] = {}
- for provider in PROVIDERS:
- configured = provider in self._configured_providers
- if not configured:
- results[provider] = {"configured": False, "status": "not_configured", "error": None}
- continue
- try:
- async with self._backend(provider) as backend:
- await backend.health_check()
- results[provider] = {"configured": True, "status": "ok", "error": None}
- except Exception as exc:
- logger.warning(
- "vector store provider_health error",
- provider=provider,
- error_type=type(exc).__name__,
- )
- results[provider] = {
- "configured": True,
- "status": "error",
- "error": sanitize_message_text(type(exc).__name__),
- }
- return results
-
- def _client(self, provider: VectorStoreProvider | None = None) -> Any:
- backend = self.backends[validate_provider(provider or self._fallback_provider)]
- if isinstance(backend, QdrantVectorStore):
- return backend._client()
- client = getattr(backend, "client", None)
+ def _client(self) -> Any:
+ client = getattr(self.backend, "client", None)
if client is None:
- backend.connect()
- client = getattr(backend, "client", None)
+ self.backend.connect()
+ client = getattr(self.backend, "client", None)
return client
- async def _provider_for(
- self,
- collection: str,
- provider: VectorStoreProvider | None,
- ) -> VectorStoreProvider:
- if provider is not None:
- return validate_provider(provider)
- try:
- from bigrag.services.collection_cache import get_or_404
-
- value = (await get_or_404(collection)).get("vector_store_provider")
- if value:
- return validate_provider(str(value))
- except Exception:
- return self._fallback_provider
- return self._fallback_provider
-
def _sync_client(self) -> None:
- fallback = self.backends.get(self._fallback_provider)
- self.client = getattr(fallback, "client", None)
- if self.client is not None:
- return
- for provider in self.configured_providers:
- client = getattr(self.backends[provider], "client", None)
- if client is not None:
- self.client = client
- return
+ self.client = getattr(self.backend, "client", None)
async def create_collection(
self,
@@ -226,19 +114,17 @@ async def create_collection(
dimension: int,
index_type: str = "HNSW",
tenant_field: str | None = None,
- provider: VectorStoreProvider | None = None,
+ **_: Any,
) -> None:
- selected_provider = await self._provider_for(name, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
await backend.create_collection(name, dimension, index_type, tenant_field)
async def delete_collection(
self,
name: str,
- provider: VectorStoreProvider | None = None,
+ **_: Any,
) -> None:
- selected_provider = await self._provider_for(name, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
await backend.delete_collection(name)
async def insert(
@@ -250,10 +136,9 @@ async def insert(
texts: list[str],
embeddings: list[list[float]],
metadata: list[dict] | None = None,
- provider: VectorStoreProvider | None = None,
+ **_: Any,
) -> int:
- selected_provider = await self._provider_for(collection, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
return await backend.insert(
collection,
ids,
@@ -270,11 +155,10 @@ async def search(
query_embedding: list[float],
top_k: int = 10,
filters: FilterExpression | None = None,
- provider: VectorStoreProvider | None = None,
payload_fields: list[str] | None = None,
+ **_: Any,
) -> list[dict]:
- selected_provider = await self._provider_for(collection, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
return await backend.search(
collection,
query_embedding,
@@ -289,30 +173,27 @@ async def get_chunks(
document_id: str,
limit: int = 10000,
offset: int = 0,
- provider: VectorStoreProvider | None = None,
+ **_: Any,
) -> tuple[list[dict], int]:
- selected_provider = await self._provider_for(collection, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
return await backend.get_chunks(collection, document_id, limit, offset)
async def delete_by_document(
self,
collection: str,
document_id: str,
- provider: VectorStoreProvider | None = None,
+ **_: Any,
) -> None:
- selected_provider = await self._provider_for(collection, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
await backend.delete_by_document(collection, document_id)
async def delete_by_ids(
self,
collection: str,
ids: list[str],
- provider: VectorStoreProvider | None = None,
+ **_: Any,
) -> None:
- selected_provider = await self._provider_for(collection, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
await backend.delete_by_ids(collection, ids)
async def text_search(
@@ -321,10 +202,9 @@ async def text_search(
query_terms: list[str],
top_k: int = 10,
filters: FilterExpression | None = None,
- provider: VectorStoreProvider | None = None,
+ **_: Any,
) -> list[dict]:
- selected_provider = await self._provider_for(collection, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
return await backend.text_search(collection, query_terms, top_k, filters)
async def upsert(
@@ -334,10 +214,9 @@ async def upsert(
embeddings: list[list[float]],
texts: list[str],
metadata: list[dict] | None = None,
- provider: VectorStoreProvider | None = None,
+ **_: Any,
) -> int:
- selected_provider = await self._provider_for(collection, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
return await backend.upsert(collection, ids, embeddings, texts, metadata)
async def export_collection_points(
@@ -345,10 +224,9 @@ async def export_collection_points(
collection: str,
*,
with_vectors: bool = True,
- provider: VectorStoreProvider | None = None,
+ **_: Any,
) -> list[dict]:
- selected_provider = await self._provider_for(collection, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
return await backend.export_collection_points(collection, with_vectors=with_vectors)
async def iter_collection_points(
@@ -356,10 +234,9 @@ async def iter_collection_points(
collection: str,
*,
with_vectors: bool = True,
- provider: VectorStoreProvider | None = None,
+ **_: Any,
):
- selected_provider = await self._provider_for(collection, provider)
- async with self._backend(selected_provider) as backend:
+ async with self._backend() as backend:
async for point in backend.iter_collection_points(
collection,
with_vectors=with_vectors,
diff --git a/api/bigrag/services/vector_store/qdrant.py b/api/bigrag/services/vector_store/qdrant.py
deleted file mode 100644
index 3c72ba31..00000000
--- a/api/bigrag/services/vector_store/qdrant.py
+++ /dev/null
@@ -1,499 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-from collections.abc import Awaitable, Callable
-from typing import Any
-
-import httpx
-from qdrant_client import AsyncQdrantClient, models
-
-from bigrag.logging import get_logger
-from bigrag.services._retrieval_filters import FilterExpression
-from bigrag.services.vector_store.base import (
- VectorStoreProvider,
- _backend_name,
- _build_payload,
- _chunk_rows_from_payloads,
- _point_id,
- _row_from_payload,
-)
-from bigrag.services.vector_store.qdrant_filter import combine_filters, to_qdrant_filter
-
-logger = get_logger("bigrag.vector_store")
-
-_TRANSIENT_ERRORS = (
- ConnectionError,
- TimeoutError,
- OSError,
- httpx.HTTPError,
-)
-
-
-class QdrantVectorStore:
- provider: VectorStoreProvider = "qdrant"
- supports_text_search = True
-
- def __init__(
- self,
- url: str = "http://localhost:6333",
- *,
- connect_timeout_seconds: int | float | None = 10,
- search_ef: int | None = None,
- prefix: str = "bigrag_",
- prefer_grpc: bool = False,
- grpc_port: int = 6334,
- ) -> None:
- self.url = url
- self.client: AsyncQdrantClient | None = None
- self._max_retries: int = 2
- self._connect_timeout_seconds = (
- None
- if connect_timeout_seconds is None or connect_timeout_seconds <= 0
- else float(connect_timeout_seconds)
- )
- self._search_ef = search_ef if search_ef and search_ef > 0 else None
- self.prefix = prefix
- self._prefer_grpc = bool(prefer_grpc)
- self._grpc_port = int(grpc_port) if grpc_port else 6334
-
- def connect(self) -> None:
- self.client = AsyncQdrantClient(
- url=self.url,
- timeout=self._connect_timeout_seconds,
- prefer_grpc=self._prefer_grpc,
- grpc_port=self._grpc_port,
- )
- logger.info(
- "connected to qdrant",
- url=self.url,
- prefer_grpc=self._prefer_grpc,
- grpc_port=self._grpc_port,
- )
-
- async def reconnect(self) -> None:
- logger.warning("reconnecting to qdrant", url=self.url)
- await self.close()
- self.connect()
- logger.info("reconnected to qdrant", url=self.url)
-
- async def _run_with_retry(
- self,
- fn: Callable[..., Awaitable[Any]],
- *args: Any,
- **kwargs: Any,
- ) -> Any:
- last_error = None
- for attempt in range(self._max_retries + 1):
- try:
- return await fn(*args, **kwargs)
- except _TRANSIENT_ERRORS as e:
- last_error = e
- if attempt < self._max_retries:
- logger.warning(
- "qdrant transient error",
- attempt=attempt + 1,
- max_attempts=self._max_retries + 1,
- error=repr(e),
- )
- await self.reconnect()
- else:
- raise
- except Exception as e:
- err_str = str(e).lower()
- if any(kw in err_str for kw in ("connect", "timeout", "unavailable", "reset")):
- last_error = e
- if attempt < self._max_retries:
- logger.warning(
- "qdrant likely transient error",
- attempt=attempt + 1,
- max_attempts=self._max_retries + 1,
- error=repr(e),
- )
- await self.reconnect()
- else:
- raise
- else:
- raise
- raise last_error
-
- async def close(self) -> None:
- if self.client:
- await self.client.close()
- self.client = None
- logger.info("qdrant connection closed")
-
- async def health_check(self) -> None:
- client = self._client()
- await self._run_with_retry(client.get_collections)
-
- def _client(self) -> AsyncQdrantClient:
- if self.client is None:
- self.connect()
- if self.client is None:
- raise RuntimeError("Qdrant client is not connected")
- return self.client
-
- def _col(self, name: str) -> str:
- return _backend_name(self.prefix, name)
-
- async def _create_payload_index(
- self,
- collection_name: str,
- field_name: str,
- schema: Any,
- ) -> None:
- client = self._client()
- try:
- await self._run_with_retry(
- client.create_payload_index,
- collection_name=collection_name,
- field_name=field_name,
- field_schema=schema,
- wait=True,
- )
- except Exception as exc:
- if "already exists" in str(exc).lower() or "exists" in str(exc).lower():
- return
- logger.warning(
- "vector_store: payload index creation failed",
- collection=collection_name,
- field=field_name,
- error=str(exc),
- )
-
- async def _ensure_payload_indexes(
- self,
- collection_name: str,
- tenant_field: str | None = None,
- ) -> None:
- text_schema = models.TextIndexParams(
- type=models.TextIndexType.TEXT,
- tokenizer=models.TokenizerType.WORD,
- min_token_len=2,
- lowercase=True,
- )
- indexes: list[tuple[str, Any]] = [
- ("id", "keyword"),
- ("document_id", "keyword"),
- ("chunk_index", "integer"),
- ("char_start", "integer"),
- ("char_end", "integer"),
- ("page_no", "integer"),
- ("text", text_schema),
- ]
- if tenant_field:
- indexes.append((tenant_field, "keyword"))
-
- await asyncio.gather(
- *[
- self._create_payload_index(collection_name, field_name, schema)
- for field_name, schema in indexes
- ]
- )
-
- async def create_collection(
- self,
- name: str,
- dimension: int,
- index_type: str = "HNSW",
- tenant_field: str | None = None,
- ) -> None:
- col = self._col(name)
- client = self._client()
-
- if not await self._run_with_retry(client.collection_exists, col):
- await self._run_with_retry(
- client.create_collection,
- collection_name=col,
- vectors_config=models.VectorParams(
- size=dimension,
- distance=models.Distance.COSINE,
- ),
- )
- logger.info(
- "created qdrant collection",
- collection=col,
- dimension=dimension,
- index=index_type,
- )
-
- await self._ensure_payload_indexes(col, tenant_field=tenant_field)
-
- async def delete_collection(self, name: str) -> None:
- col = self._col(name)
- client = self._client()
- if await self._run_with_retry(client.collection_exists, col):
- await self._run_with_retry(client.delete_collection, col)
- logger.info("dropped qdrant collection", collection=col)
-
- async def insert(
- self,
- collection: str,
- ids: list[str],
- document_ids: list[str],
- chunk_indices: list[int],
- texts: list[str],
- embeddings: list[list[float]],
- metadata: list[dict] | None = None,
- ) -> int:
- col = self._col(collection)
- points = []
- for i in range(len(ids)):
- points.append(
- models.PointStruct(
- id=_point_id(col, ids[i]),
- vector=embeddings[i],
- payload=_build_payload(
- id_=ids[i],
- document_id=document_ids[i],
- chunk_index=chunk_indices[i],
- text=texts[i],
- metadata=metadata[i] if metadata else None,
- ),
- )
- )
-
- client = self._client()
- await self._run_with_retry(client.upsert, collection_name=col, points=points, wait=True)
- logger.info("inserted vectors", collection=col, count=len(points))
- return len(points)
-
- def _search_params(self) -> models.SearchParams | None:
- if self._search_ef is None:
- return None
- return models.SearchParams(hnsw_ef=self._search_ef)
-
- @staticmethod
- def _row_from_qdrant(point: Any) -> dict:
- payload = dict(getattr(point, "payload", None) or {})
- point_id = str(getattr(point, "id", ""))
- return _row_from_payload(point_id, getattr(point, "score", 0.0), payload)
-
- async def search(
- self,
- collection: str,
- query_embedding: list[float],
- top_k: int = 10,
- filters: FilterExpression | None = None,
- payload_fields: list[str] | None = None,
- ) -> list[dict]:
- col = self._col(collection)
-
- with_payload: Any = (
- models.PayloadSelectorInclude(include=list(payload_fields)) if payload_fields else True
- )
-
- client = self._client()
- results = await self._run_with_retry(
- client.query_points,
- collection_name=col,
- query=query_embedding,
- limit=top_k,
- query_filter=to_qdrant_filter(filters),
- search_params=self._search_params(),
- with_payload=with_payload,
- with_vectors=False,
- )
-
- hits = [self._row_from_qdrant(point) for point in results.points]
- logger.info("vector search", collection=col, top_k=top_k, hits=len(hits), filters=filters)
- return hits
-
- async def get_chunks(
- self,
- collection: str,
- document_id: str,
- limit: int = 10000,
- offset: int = 0,
- ) -> tuple[list[dict], int]:
- col = self._col(collection)
- client = self._client()
- if not await self._run_with_retry(client.collection_exists, col):
- return [], 0
-
- doc_filter = models.Filter(
- must=[
- models.FieldCondition(
- key="document_id",
- match=models.MatchValue(value=document_id),
- )
- ]
- )
-
- try:
- count_resp = await self._run_with_retry(
- client.count,
- collection_name=col,
- count_filter=doc_filter,
- exact=True,
- )
- total = int(getattr(count_resp, "count", 0))
- except Exception:
- total = 0
-
- needed = offset + max(limit, 0)
- if needed <= 0:
- return [], total
-
- results = []
- next_offset = None
- page_size = min(max(needed, 256), 10000)
- while True:
- batch, next_offset = await self._run_with_retry(
- client.scroll,
- collection_name=col,
- scroll_filter=doc_filter,
- with_payload=True,
- with_vectors=False,
- limit=page_size,
- offset=next_offset,
- )
- results.extend(batch)
- if next_offset is None:
- break
- if total and len(results) >= total:
- break
- payloads = [r.payload or {} for r in results]
- rows, computed_total = _chunk_rows_from_payloads(payloads, limit, offset)
- return rows, total or computed_total
-
- async def delete_by_document(self, collection: str, document_id: str) -> None:
- col = self._col(collection)
- client = self._client()
- if not await self._run_with_retry(client.collection_exists, col):
- return
- await self._run_with_retry(
- client.delete,
- collection_name=col,
- points_selector=models.Filter(
- must=[
- models.FieldCondition(
- key="document_id",
- match=models.MatchValue(value=document_id),
- )
- ]
- ),
- wait=True,
- )
- logger.info("delete vectors by document", collection=col, document_id=document_id)
-
- async def delete_by_ids(self, collection: str, ids: list[str]) -> None:
- col = self._col(collection)
- client = self._client()
- point_ids = [_point_id(col, id_) for id_ in ids]
- await self._run_with_retry(
- client.delete,
- collection_name=col,
- points_selector=point_ids,
- wait=True,
- )
- logger.info("delete vectors by ids", collection=col, count=len(ids))
-
- async def text_search(
- self,
- collection: str,
- query_terms: list[str],
- top_k: int = 10,
- filters: FilterExpression | None = None,
- ) -> list[dict]:
- col = self._col(collection)
- terms = [term for term in query_terms if term]
- if not terms:
- return []
-
- text_filter = models.Filter(
- should=[
- models.FieldCondition(key="text", match=models.MatchText(text=term))
- for term in terms
- ]
- )
- combined_filter = combine_filters(to_qdrant_filter(filters), text_filter)
-
- try:
- client = self._client()
- results, _next_offset = await self._run_with_retry(
- client.scroll,
- collection_name=col,
- scroll_filter=combined_filter,
- with_payload=True,
- with_vectors=False,
- limit=top_k * 10,
- )
- except _TRANSIENT_ERRORS:
- raise
- except Exception as exc:
- logger.warning("text search query failed", collection=col, error=repr(exc))
- return []
-
- logger.info("text search", collection=col, terms=len(terms), hits=len(results))
- return [self._row_from_qdrant(point) for point in results]
-
- async def upsert(
- self,
- collection: str,
- ids: list[str],
- embeddings: list[list[float]],
- texts: list[str],
- metadata: list[dict] | None = None,
- ) -> int:
- col = self._col(collection)
- points = []
- for i in range(len(ids)):
- points.append(
- models.PointStruct(
- id=_point_id(col, ids[i]),
- vector=embeddings[i],
- payload=_build_payload(
- id_=ids[i],
- document_id="",
- chunk_index=0,
- text=texts[i],
- metadata=metadata[i] if metadata else None,
- ),
- )
- )
-
- client = self._client()
- await self._run_with_retry(client.upsert, collection_name=col, points=points, wait=True)
- logger.info("upserted vectors", collection=col, count=len(points))
- return len(points)
-
- async def export_collection_points(
- self,
- collection: str,
- *,
- with_vectors: bool = True,
- ) -> list[dict]:
- return [
- point
- async for point in self.iter_collection_points(collection, with_vectors=with_vectors)
- ]
-
- async def iter_collection_points(
- self,
- collection: str,
- *,
- with_vectors: bool = True,
- ):
- col = self._col(collection)
- client = self._client()
- if not await self._run_with_retry(client.collection_exists, col):
- return
- offset = None
- while True:
- points, offset = await self._run_with_retry(
- client.scroll,
- collection_name=col,
- limit=256,
- offset=offset,
- with_payload=True,
- with_vectors=with_vectors,
- )
- for point in points:
- yield {
- "id": str(getattr(point, "id", "")),
- "payload": getattr(point, "payload", {}) or {},
- "vector": getattr(point, "vector", None) if with_vectors else None,
- }
- if offset is None:
- break
diff --git a/api/bigrag/services/vector_store/qdrant_filter.py b/api/bigrag/services/vector_store/qdrant_filter.py
deleted file mode 100644
index ae1c44c0..00000000
--- a/api/bigrag/services/vector_store/qdrant_filter.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from __future__ import annotations
-
-from qdrant_client import models
-
-from bigrag.services._retrieval_filters import FilterExpression
-
-
-def to_qdrant_filter(filters: FilterExpression | None) -> models.Filter | None:
- if filters is None:
- return None
- must: list[models.Condition] = []
- must_not: list[models.Condition] = []
- for condition in filters.conditions:
- if condition.operator == "eq":
- must.append(
- models.FieldCondition(
- key=condition.field,
- match=models.MatchValue(value=condition.value),
- )
- )
- elif condition.operator == "ne":
- must_not.append(
- models.FieldCondition(
- key=condition.field,
- match=models.MatchValue(value=condition.value),
- )
- )
- elif condition.operator == "in":
- must.append(
- models.FieldCondition(
- key=condition.field,
- match=models.MatchAny(any=condition.value),
- )
- )
- else:
- must.append(
- models.FieldCondition(
- key=condition.field,
- range=models.Range(
- gt=condition.value if condition.operator == "gt" else None,
- gte=condition.value if condition.operator == "gte" else None,
- lt=condition.value if condition.operator == "lt" else None,
- lte=condition.value if condition.operator == "lte" else None,
- ),
- )
- )
- if not must and not must_not:
- return None
- return models.Filter(must=must or None, must_not=must_not or None)
-
-
-def combine_filters(
- *filters: models.Filter | None,
-) -> models.Filter | None:
- active = [f for f in filters if f is not None]
- if not active:
- return None
- if len(active) == 1:
- return active[0]
- return models.Filter(must=active)
diff --git a/api/bigrag/services/vector_store/turbopuffer.py b/api/bigrag/services/vector_store/turbopuffer.py
index 45d15492..5d25966e 100644
--- a/api/bigrag/services/vector_store/turbopuffer.py
+++ b/api/bigrag/services/vector_store/turbopuffer.py
@@ -7,7 +7,6 @@
from bigrag.logging import get_logger
from bigrag.services._retrieval_filters import FilterCondition, FilterExpression
from bigrag.services.vector_store.base import (
- VectorStoreFeatureError,
VectorStoreProvider,
_backend_name,
_build_payload,
@@ -53,7 +52,7 @@ def _schema(dimension: int) -> dict:
_PUBLIC_ID_FIELD: {"type": "string"},
"document_id": {"type": "string"},
"chunk_index": {"type": "int"},
- "text": {"type": "string", "filterable": False},
+ "text": {"type": "string", "full_text_search": True},
}
@@ -69,7 +68,6 @@ def _row_payload(row: dict) -> dict:
class TurbopufferVectorStore:
provider: VectorStoreProvider = "turbopuffer"
- supports_text_search = False
def __init__(
self,
@@ -77,22 +75,23 @@ def __init__(
api_key: str | None,
region: str,
namespace_prefix: str = "bigrag_",
+ base_url: str | None = None,
) -> None:
self.api_key = api_key
self.region = region
self.prefix = namespace_prefix or "bigrag_"
+ self.base_url = base_url.rstrip("/") if base_url else None
self.client: httpx.AsyncClient | None = None
def connect(self) -> None:
if not self.api_key:
raise RuntimeError("turbopuffer API key is not configured")
- base_url = f"https://{self.region}.turbopuffer.com"
self.client = httpx.AsyncClient(
- base_url=base_url,
+ base_url=self.base_url or f"https://{self.region}.turbopuffer.com",
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=30,
)
- logger.info("connected to turbopuffer", region=self.region)
+ logger.info("connected to turbopuffer", region=self.region, base_url=self.base_url)
def _client(self) -> httpx.AsyncClient:
if self.client is None:
@@ -261,7 +260,26 @@ async def text_search(
top_k: int = 10,
filters: FilterExpression | None = None,
) -> list[dict]:
- raise VectorStoreFeatureError("turbopuffer does not support keyword or hybrid search in v1")
+ query = " ".join(term for term in query_terms if term).strip()
+ if not query:
+ return []
+ payload: dict[str, Any] = {
+ "rank_by": ["text", "BM25", query],
+ "top_k": top_k,
+ "exclude_attributes": ["vector"],
+ }
+ turbo_filter = _to_turbopuffer_filter(filters)
+ if turbo_filter:
+ payload["filters"] = turbo_filter
+ rows = await self._query_rows(collection, payload)
+ results = []
+ for row in rows:
+ point_id = str(row.get("id", ""))
+ score = row.get("$score")
+ if score is None:
+ score = max(0.0, 1.0 - float(row.get("$dist", 0.0)))
+ results.append(_row_from_payload(point_id, float(score), _row_payload(row)))
+ return results
async def upsert(
self,
diff --git a/api/pyproject.toml b/api/pyproject.toml
index 98a0454c..cf6e33de 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -11,7 +11,6 @@ dependencies = [
"asyncpg>=0.30.0,<1",
"sqlalchemy[asyncio]>=2.0.36,<3",
"alembic>=1.14.0,<2",
- "qdrant-client>=1.17.0,<2",
"docling>=2.25.0,<3",
"pypdfium2>=5.7.0,<6",
"huggingface-hub>=0.36.0,<2",
diff --git a/api/uv.lock b/api/uv.lock
index ed566c8f..c55d2bb2 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -210,7 +210,6 @@ dependencies = [
{ name = "pydantic-settings" },
{ name = "pypdfium2" },
{ name = "python-multipart" },
- { name = "qdrant-client" },
{ name = "redis", extra = ["hiredis"] },
{ name = "sqlalchemy", extra = ["asyncio"] },
{ name = "starlette" },
@@ -244,7 +243,6 @@ requires-dist = [
{ name = "pydantic-settings", specifier = ">=2.7.0,<3" },
{ name = "pypdfium2", specifier = ">=5.7.0,<6" },
{ name = "python-multipart", specifier = ">=0.0.18,<1" },
- { name = "qdrant-client", specifier = ">=1.17.0,<2" },
{ name = "redis", extras = ["hiredis"], specifier = ">=5.2.0,<8" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.36,<3" },
{ name = "starlette", specifier = ">=1.0.0,<2" },
@@ -931,47 +929,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" },
]
-[[package]]
-name = "grpcio"
-version = "1.80.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" },
- { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" },
- { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" },
- { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" },
- { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" },
- { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" },
- { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" },
- { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" },
- { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" },
- { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" },
- { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" },
- { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" },
- { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" },
- { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" },
- { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" },
- { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" },
- { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" },
- { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" },
- { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" },
- { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" },
- { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" },
- { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" },
- { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" },
- { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" },
- { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" },
- { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" },
- { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" },
- { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" },
- { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" },
- { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" },
-]
-
[[package]]
name = "h11"
version = "0.16.0"
@@ -981,19 +938,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
-[[package]]
-name = "h2"
-version = "4.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "hpack" },
- { name = "hyperframe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
-]
-
[[package]]
name = "hf-xet"
version = "1.5.0"
@@ -1086,15 +1030,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/35/d6/191e6741addc97bcf5e755661f8c82f0fd0aa35f07ece56e858da689b57e/hiredis-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ab1f646ff531d70bfd25f01e60708dfa3d105eb458b7dedd9fe9a443039fd809", size = 23811, upload-time = "2026-03-16T15:20:34.292Z" },
]
-[[package]]
-name = "hpack"
-version = "4.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
-]
-
[[package]]
name = "httpcore"
version = "1.0.9"
@@ -1152,11 +1087,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
-[package.optional-dependencies]
-http2 = [
- { name = "h2" },
-]
-
[[package]]
name = "httpx-sse"
version = "0.4.3"
@@ -1186,15 +1116,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" },
]
-[[package]]
-name = "hyperframe"
-version = "6.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
-]
-
[[package]]
name = "idna"
version = "3.15"
@@ -1377,6 +1298,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" },
{ url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" },
{ url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" },
+ { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" },
{ url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" },
{ url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" },
@@ -1394,6 +1316,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" },
{ url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" },
{ url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" },
{ url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" },
{ url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" },
{ url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" },
@@ -1411,6 +1334,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" },
{ url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" },
{ url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" },
{ url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" },
{ url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" },
{ url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" },
@@ -1428,6 +1352,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" },
{ url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" },
{ url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
]
[[package]]
@@ -2094,33 +2019,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/34/b6f19941adcdaf415b5e8a8d577499f5b6a76b59cbae37f9b125a9ffe9f2/polyfactory-3.3.0-py3-none-any.whl", hash = "sha256:686abcaa761930d3df87b91e95b26b8d8cb9fdbbbe0b03d5f918acff5c72606e", size = 62707, upload-time = "2026-02-22T09:46:25.985Z" },
]
-[[package]]
-name = "portalocker"
-version = "3.2.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pywin32", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" },
-]
-
-[[package]]
-name = "protobuf"
-version = "7.34.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" },
- { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" },
- { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" },
- { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" },
- { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" },
- { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" },
- { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" },
-]
-
[[package]]
name = "psutil"
version = "7.2.2"
@@ -2466,24 +2364,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
-[[package]]
-name = "qdrant-client"
-version = "1.18.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "grpcio" },
- { name = "httpx", extra = ["http2"] },
- { name = "numpy" },
- { name = "portalocker" },
- { name = "protobuf" },
- { name = "pydantic" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/65/45/5b1bdd15a3c7730eefb9c113600829e20d689b82b5a23f9e07d107094004/qdrant_client-1.18.0.tar.gz", hash = "sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4", size = 352580, upload-time = "2026-05-11T14:12:38.702Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d6/10/c437bd2ac41ef30d3019063e6ce537dc111e9214473b337ee88f7fa6359a/qdrant_client-1.18.0-py3-none-any.whl", hash = "sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd", size = 398126, upload-time = "2026-05-11T14:12:36.998Z" },
-]
-
[[package]]
name = "rapidocr"
version = "3.8.1"
diff --git a/app/src/components/navigation/sidebar.tsx b/app/src/components/navigation/sidebar.tsx
index 0cf14854..f9f1430e 100644
--- a/app/src/components/navigation/sidebar.tsx
+++ b/app/src/components/navigation/sidebar.tsx
@@ -7,7 +7,6 @@ import {
BookOpen,
Cloud,
Cpu,
- Database,
FlaskConical,
HardDrive,
KeyRound,
@@ -68,7 +67,6 @@ const NAV_GROUPS: readonly NavGroup[] = [
items: [
{ admin: true, href: "/backups", icon: Archive, label: "Backups" },
{ admin: true, href: "/data-storage", icon: HardDrive, label: "Data Storage" },
- { admin: true, href: "/vector-storage", icon: Database, label: "Vector Storage" },
{ admin: true, href: "/settings", icon: Settings, label: "Settings" },
],
},
diff --git a/app/src/features/collections/collection-form-state.ts b/app/src/features/collections/collection-form-state.ts
index 7ab2e28a..6cdb5d66 100644
--- a/app/src/features/collections/collection-form-state.ts
+++ b/app/src/features/collections/collection-form-state.ts
@@ -10,7 +10,6 @@ export type CreateCollectionFormValues = {
presetId: string;
tenantGuardEnabled: boolean;
tenantField: string;
- vectorStoreProvider: "qdrant" | "turbopuffer";
};
export type CollectionSearchMode = "semantic" | "keyword" | "hybrid";
@@ -34,7 +33,6 @@ export const defaultCreateCollectionFormValues = (): CreateCollectionFormValues
presetId: "",
tenantGuardEnabled: false,
tenantField: "",
- vectorStoreProvider: "qdrant",
});
export const defaultCollectionSearchFormValues = (): CollectionSearchFormValues => ({
@@ -118,7 +116,6 @@ export const createCollectionBodyFromValues = ({
presetId,
tenantGuardEnabled,
tenantField,
- vectorStoreProvider,
}: CreateCollectionFormValues) => ({
chunk_overlap: chunkOverlap,
chunk_size: chunkSize,
@@ -129,7 +126,6 @@ export const createCollectionBodyFromValues = ({
multimodal_enrichment_enabled: multimodalEnrichmentEnabled,
name: normalizeCollectionName(name),
tenant_field: tenantGuardEnabled ? tenantField.trim() || null : null,
- vector_store_provider: vectorStoreProvider,
});
export const collectionSearchBodyFromValues = ({
diff --git a/app/src/features/collections/create-collection-modal.tsx b/app/src/features/collections/create-collection-modal.tsx
index 1d46fe0d..dbfa9233 100644
--- a/app/src/features/collections/create-collection-modal.tsx
+++ b/app/src/features/collections/create-collection-modal.tsx
@@ -147,18 +147,6 @@ export const CreateCollectionModal = ({ open, onClose }: Props) => {
/>
)}
-
- {(field) => (
-
{presetsPending ? (
@@ -386,11 +374,6 @@ export const CreateCollectionModal = ({ open, onClose }: Props) => {
);
};
-const VECTOR_STORAGE_OPTIONS = [
- { value: "qdrant", label: "Qdrant" },
- { value: "turbopuffer", label: "Turbopuffer" },
-] as const;
-
const DEFAULT_METADATA_SCHEMA = '{\n "type": "object"\n}';
const useDefaultEmbeddingPreset = (
diff --git a/app/src/features/overview/overview-page.tsx b/app/src/features/overview/overview-page.tsx
index 369799cd..493cedaa 100644
--- a/app/src/features/overview/overview-page.tsx
+++ b/app/src/features/overview/overview-page.tsx
@@ -70,12 +70,7 @@ export const OverviewPage = () => {
const services = useMemo(
() => [
{ label: "Postgres", ok: readiness?.postgres },
- {
- label: readiness?.vector_store_provider
- ? `Vector store (${readiness.vector_store_provider.replace("_", " ")})`
- : "Vector store",
- ok: readiness?.vector_store,
- },
+ { label: "Vector store", ok: readiness?.vector_store },
{ label: "Redis", ok: readiness?.redis },
{ detail: readiness?.embedding_error, label: "Embeddings", ok: readiness?.embedding },
{
diff --git a/app/src/features/settings/instance-settings-helpers.ts b/app/src/features/settings/instance-settings-helpers.ts
index b3da822c..db431005 100644
--- a/app/src/features/settings/instance-settings-helpers.ts
+++ b/app/src/features/settings/instance-settings-helpers.ts
@@ -13,7 +13,6 @@ const examplePlaceholders: Record
= {
chat_base_url: "https://api.openai.com/v1",
embedding_api_key: "Paste API key",
embedding_base_url: "https://api.openai.com/v1",
- qdrant_search_ef: "Optional, e.g. 128",
storage_s3_access_key_id: "Access key ID",
storage_s3_bucket: "bigrag-documents",
storage_s3_endpoint_url: "https://account-id.r2.cloudflarestorage.com",
diff --git a/app/src/features/settings/settings-layout.ts b/app/src/features/settings/settings-layout.ts
index f20ce364..a806a65a 100644
--- a/app/src/features/settings/settings-layout.ts
+++ b/app/src/features/settings/settings-layout.ts
@@ -141,20 +141,12 @@ const SETTINGS_GROUP_LAYOUTS: Record
title: "File storage",
},
vector_store: {
- commonKeys: [
- "qdrant_url",
- "qdrant_connect_timeout_seconds",
- "qdrant_required",
- "qdrant_search_ef",
- "turbopuffer_api_key",
- "turbopuffer_region",
- "turbopuffer_namespace_prefix",
- ],
- description: "Vector backend connection details and provider credentials.",
+ commonKeys: ["turbopuffer_api_key", "turbopuffer_region", "turbopuffer_namespace_prefix"],
+ description: "Turbopuffer connection details and provider credentials.",
emptyState: "Vector storage settings are not available from this API.",
eyebrow: "Indexes",
group: "vector_store",
- recommendedAction: "Keep connection details ready for the providers collections can select.",
+ recommendedAction: "Keep the API key, region, and namespace prefix aligned with deployment.",
title: "Vector storage",
},
webhooks: {
diff --git a/app/src/features/settings/tabs/server-tab.tsx b/app/src/features/settings/tabs/server-tab.tsx
index 814e9be7..234df441 100644
--- a/app/src/features/settings/tabs/server-tab.tsx
+++ b/app/src/features/settings/tabs/server-tab.tsx
@@ -114,11 +114,7 @@ export const ServerTab = () => {
diff --git a/app/src/features/vector-storage/vector-migration-panel.tsx b/app/src/features/vector-storage/vector-migration-panel.tsx
deleted file mode 100644
index c73e5f19..00000000
--- a/app/src/features/vector-storage/vector-migration-panel.tsx
+++ /dev/null
@@ -1,370 +0,0 @@
-import { useQueryClient } from "@tanstack/react-query";
-import { ArrowRightLeft, CircleStop, Cloud, Database, Trash2 } from "lucide-react";
-import { useEffect, useMemo, useState } from "react";
-import { toast } from "sonner";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { ConfirmDialog } from "@/components/ui/confirm-dialog";
-import { Empty } from "@/components/ui/empty";
-import { Modal } from "@/components/ui/modal";
-import {
- useDeleteVectorMigration,
- useStartVectorMigration,
- useVectorMigrations,
- useVectorStorageOverview,
-} from "@/hooks/use-vector-migrations";
-import { formatNumber, formatRelative } from "@/lib/format";
-import { queryKeys } from "@/lib/query-keys";
-import type { Collection, VectorMigrationJob, VectorMigrationProvider } from "@/types/bigrag";
-
-type VectorMigrationPanelProps = {
- readonly collection?: Collection;
-};
-
-type MigrationTarget = {
- readonly collection: string;
- readonly source: VectorMigrationProvider;
- readonly target: VectorMigrationProvider;
-};
-
-const PROVIDER_META: Record = {
- qdrant: { label: "Qdrant", icon: Database },
- turbopuffer: { label: "turbopuffer", icon: Cloud },
-};
-
-const targetProvider = (provider: VectorMigrationProvider): VectorMigrationProvider =>
- provider === "qdrant" ? "turbopuffer" : "qdrant";
-
-const providerLabel = (provider: VectorMigrationProvider) => PROVIDER_META[provider].label;
-
-const activeStatuses = new Set(["pending", "running", "canceling"]);
-const migrationDescription =
- "Move a collection between vector providers. Writes are paused during the job, then old source vectors are deleted after cutover.";
-
-export const VectorMigrationPanel = ({ collection }: VectorMigrationPanelProps) => {
- return (
-
-
-
-
- Vector migration
-
- {migrationDescription}
-
-
-
-
-
- );
-};
-
-export const VectorMigrationModal = ({
- collection,
- onClose,
- open,
-}: VectorMigrationPanelProps & {
- readonly onClose: () => void;
- readonly open: boolean;
-}) => (
-
-
-
{migrationDescription}
-
-
-
-);
-
-const VectorMigrationContent = ({ collection }: VectorMigrationPanelProps) => {
- const queryClient = useQueryClient();
- const overview = useVectorStorageOverview();
- const migrations = useVectorMigrations({ collection: collection?.name });
- const startMigration = useStartVectorMigration();
- const deleteMigration = useDeleteVectorMigration();
- const [target, setTarget] = useState(null);
- const [deleteTarget, setDeleteTarget] = useState(null);
-
- const configuredProviders = overview.data?.configured_providers ?? [];
- const rows = useMemo(() => {
- if (collection) {
- return [
- {
- name: collection.name,
- provider: collection.vector_store_provider,
- documents: collection.document_count,
- chunks: null,
- },
- ];
- }
- return (
- overview.data?.collections.map((item) => ({
- name: item.name,
- provider: item.provider,
- documents: item.documents,
- chunks: item.chunks,
- })) ?? []
- );
- }, [collection, overview.data?.collections]);
- const jobs = migrations.data?.jobs ?? [];
- const activeJob = jobs.find((job) => activeStatuses.has(job.status));
- const completionKey = jobs
- .filter((job) => !activeStatuses.has(job.status))
- .map((job) => `${job.id}:${job.status}:${job.updated_at}`)
- .join("|");
-
- useEffect(() => {
- if (!completionKey) return;
- queryClient.invalidateQueries({ queryKey: queryKeys.collections.all() });
- queryClient.invalidateQueries({ queryKey: queryKeys.vectorStorageOverview() });
- if (collection) {
- queryClient.invalidateQueries({
- queryKey: queryKeys.collections.one({ name: collection.name }),
- });
- }
- }, [collection, completionKey, queryClient]);
-
- return (
- <>
-
- {rows.length ? (
-
-
- Collection
- Current
- Action
-
-
- {rows.map((row) => {
- const nextProvider = targetProvider(row.provider);
- const ready = configuredProviders.includes(nextProvider);
- return (
-
-
-
{row.name}
-
- {formatNumber(row.documents)} documents
- {row.chunks === null ? "" : ` · ${formatNumber(row.chunks)} chunks`}
-
-
-
-
-
- );
- })}
-
-
- ) : (
-
}
- title="No collections"
- description="Create a collection before starting a vector migration."
- bordered={false}
- className="rounded-md border border-dashed border-border bg-muted/40"
- />
- )}
-
-
- setTarget(null)}
- title={target ? `Migrate ${target.collection}?` : "Migrate collection?"}
- description={
- target
- ? `bigRAG will pause writes, copy vectors from ${providerLabel(target.source)} to ${providerLabel(target.target)}, switch the collection, and delete the old ${providerLabel(
- target.source,
- )} vectors.`
- : ""
- }
- confirmLabel="Start migration"
- confirmationLabel={target ? `Type ${target.collection} to start migration` : undefined}
- confirmationText={target?.collection}
- loading={startMigration.isPending}
- onConfirm={async () => {
- if (!target) return;
- await startMigration.mutateAsync({
- collection: target.collection,
- target_provider: target.target,
- });
- setTarget(null);
- }}
- />
- setDeleteTarget(null)}
- title={deleteTarget ? deleteTitle(deleteTarget) : "Delete migration?"}
- description={deleteTarget ? deleteDescription(deleteTarget) : ""}
- confirmLabel={
- deleteTarget && activeStatuses.has(deleteTarget.status) ? "Stop and delete" : "Delete"
- }
- confirmationLabel={
- deleteTarget
- ? `Type ${deleteTarget.collection_name} to ${activeStatuses.has(deleteTarget.status) ? "stop and delete" : "delete"} migration`
- : undefined
- }
- confirmationText={deleteTarget?.collection_name}
- loading={deleteMigration.isPending}
- onConfirm={async () => {
- if (!deleteTarget) return;
- try {
- await deleteMigration.mutateAsync(deleteTarget);
- setDeleteTarget(null);
- } catch (err) {
- toast.error(err instanceof Error ? err.message : "Failed");
- }
- }}
- />
- >
- );
-};
-
-const ProviderBadge = ({ provider }: { readonly provider: VectorMigrationProvider }) => {
- const Icon = PROVIDER_META[provider].icon;
- return (
-
-
- {providerLabel(provider)}
-
- );
-};
-
-const MigrationJobs = ({
- deleting,
- jobs,
- onDelete,
-}: {
- readonly deleting: boolean;
- readonly jobs: VectorMigrationJob[];
- readonly onDelete: (job: VectorMigrationJob) => void;
-}) => {
- if (!jobs.length) {
- return (
- }
- title="No migration jobs"
- description="Completed and failed migrations appear here."
- bordered={false}
- className="rounded-md border border-dashed border-border bg-muted/40"
- />
- );
- }
- return (
-
-
- Migration
- Copied
- Status
- Action
-
-
- {jobs.map((job) => (
-
- ))}
-
-
- );
-};
-
-const MigrationJobRow = ({
- deleting,
- job,
- onDelete,
-}: {
- readonly deleting: boolean;
- readonly job: VectorMigrationJob;
- readonly onDelete: (job: VectorMigrationJob) => void;
-}) => (
-
-
-
-
{job.collection_name}
-
{formatRelative(job.created_at)}
-
-
- {providerLabel(job.source_provider)} to {providerLabel(job.target_provider)} · {job.phase}
-
- {job.error_message && (
-
{job.error_message}
- )}
- {activeStatuses.has(job.status) && (
-
- )}
-
-
- {formatNumber(job.copied_points)}
- {job.total_points === null ? "" : ` / ${formatNumber(job.total_points)}`}
-
-
- {job.status}
-
-
-
-);
-
-const statusVariant = (status: VectorMigrationJob["status"]) => {
- if (status === "succeeded") return "success";
- if (status === "failed") return "error";
- if (status === "running") return "primary";
- if (status === "canceling") return "error";
- return "neutral";
-};
-
-const deleteTitle = (job: VectorMigrationJob) =>
- activeStatuses.has(job.status) ? "Stop and delete migration?" : "Delete migration?";
-
-const deleteDescription = (job: VectorMigrationJob) =>
- activeStatuses.has(job.status)
- ? `Stop the ${providerLabel(job.source_provider)} to ${providerLabel(job.target_provider)} migration for ${job.collection_name} and remove it from the migration list. If cutover already started, bigRAG will finish cleanup before removing it.`
- : `Delete the ${providerLabel(job.source_provider)} to ${providerLabel(job.target_provider)} migration record for ${job.collection_name}.`;
diff --git a/app/src/features/vector-storage/vector-storage-page.tsx b/app/src/features/vector-storage/vector-storage-page.tsx
deleted file mode 100644
index 50a52155..00000000
--- a/app/src/features/vector-storage/vector-storage-page.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Cloud, Database } from "lucide-react";
-import { Page } from "@/components/ui/page";
-import { Tabs } from "@/components/ui/tabs";
-import { InstanceSettingsTab } from "@/features/settings/tabs/instance-settings-tab";
-import { VectorMigrationPanel } from "@/features/vector-storage/vector-migration-panel";
-
-export type VectorStorageProvider = "qdrant" | "turbopuffer";
-
-type VectorStoragePageProps = {
- readonly provider?: string;
- readonly onProviderChange?: (provider: VectorStorageProvider) => void;
-};
-
-const VECTOR_STORAGE_TABS = [
- { icon: Database, label: "Qdrant", value: "qdrant" },
- { icon: Cloud, label: "turbopuffer", value: "turbopuffer" },
-];
-
-const VECTOR_PROVIDER_SETTINGS: Record<
- VectorStorageProvider,
- {
- readonly description: string;
- readonly emptyState: string;
- readonly eyebrow: string;
- readonly keys: readonly string[];
- readonly recommendedAction: string;
- readonly title: string;
- }
-> = {
- qdrant: {
- description: "Qdrant connection, readiness policy, and HNSW search tuning.",
- emptyState: "Qdrant settings are not available from this API.",
- eyebrow: "Self-hosted index",
- keys: ["qdrant_url", "qdrant_connect_timeout_seconds", "qdrant_required", "qdrant_search_ef"],
- recommendedAction: "Save Qdrant when the URL and readiness policy match this deployment.",
- title: "Qdrant",
- },
- turbopuffer: {
- description: "turbopuffer credentials, region, and namespace routing.",
- emptyState: "turbopuffer settings are not available from this API.",
- eyebrow: "Managed index",
- keys: ["turbopuffer_api_key", "turbopuffer_region", "turbopuffer_namespace_prefix"],
- recommendedAction: "Save turbopuffer after the API key, region, and namespace prefix are set.",
- title: "turbopuffer",
- },
-};
-
-const getVectorStorageProvider = (value: unknown): VectorStorageProvider | undefined =>
- value === "qdrant" || value === "turbopuffer" ? value : undefined;
-
-export const VectorStoragePage = ({ provider, onProviderChange }: VectorStoragePageProps) => {
- const activeProvider = getVectorStorageProvider(provider) ?? "qdrant";
- const settings = VECTOR_PROVIDER_SETTINGS[activeProvider];
-
- return (
-
-
- {
- const nextProvider = getVectorStorageProvider(value);
- if (nextProvider) onProviderChange?.(nextProvider);
- }}
- tabs={VECTOR_STORAGE_TABS}
- value={activeProvider}
- />
-
-
-
- );
-};
diff --git a/app/src/features/vector-storage/vector-storage-route.tsx b/app/src/features/vector-storage/vector-storage-route.tsx
deleted file mode 100644
index 9d9eb9a0..00000000
--- a/app/src/features/vector-storage/vector-storage-route.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { getRouteApi, useNavigate } from "@tanstack/react-router";
-import {
- VectorStoragePage,
- type VectorStorageProvider,
-} from "@/features/vector-storage/vector-storage-page";
-
-const routeApi = getRouteApi("/_dashboard/vector-storage");
-
-export const VectorStorageRoute = () => {
- const navigate = useNavigate();
- const search = routeApi.useSearch();
-
- const setProvider = (provider: VectorStorageProvider) =>
- navigate({
- to: "/vector-storage",
- search: { provider },
- replace: true,
- });
-
- return ;
-};
diff --git a/app/src/hooks/use-collections.ts b/app/src/hooks/use-collections.ts
index f05d4694..e884937a 100644
--- a/app/src/hooks/use-collections.ts
+++ b/app/src/hooks/use-collections.ts
@@ -49,7 +49,6 @@ export const useCollectionStats = (name: string) => {
type CreateCollectionBody = {
name: string;
description?: string;
- vector_store_provider?: "qdrant" | "turbopuffer";
embedding_preset_id?: string | null;
embedding_provider?: "openai" | "openai_compatible" | "cohere" | "voyage";
embedding_model?: string;
diff --git a/app/src/hooks/use-vector-migrations.ts b/app/src/hooks/use-vector-migrations.ts
deleted file mode 100644
index 21691e36..00000000
--- a/app/src/hooks/use-vector-migrations.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { useMemo } from "react";
-import { toast } from "sonner";
-import { useSseSnapshotQuery } from "@/hooks/use-sse-snapshot-query";
-import { apiClient } from "@/lib/api";
-import { errorToast } from "@/lib/mutation-toast";
-import { queryKeys } from "@/lib/query-keys";
-import type {
- VectorMigrationJob,
- VectorMigrationJobListResponse,
- VectorMigrationProvider,
-} from "@/types/bigrag";
-
-type VectorMigrationListOptions = {
- readonly collection?: string;
-};
-
-type StatusResponse = {
- readonly status: string;
- readonly message?: string;
-};
-
-type VectorStorageOverview = {
- fallback_provider: string;
- configured_providers: VectorMigrationProvider[];
- provider_health: Record;
- collections: {
- name: string;
- provider: VectorMigrationProvider;
- documents: number;
- chunks: number;
- bytes: number;
- }[];
- totals: {
- collections: number;
- documents: number;
- chunks: number;
- bytes: number;
- };
-};
-
-export const useVectorStorageOverview = () =>
- useQuery({
- queryKey: queryKeys.vectorStorageOverview(),
- queryFn: () => apiClient.get("v1/admin/vector-storage/overview"),
- staleTime: 15_000,
- });
-
-export const useVectorMigrations = ({ collection }: VectorMigrationListOptions = {}) => {
- const queryKey = useMemo(() => queryKeys.vectorMigrations({ collection }), [collection]);
- const searchParams = collection ? { collection } : undefined;
- const path = collection
- ? `v1/admin/realtime/vector-migrations?collection=${encodeURIComponent(collection)}`
- : "v1/admin/realtime/vector-migrations";
-
- return useSseSnapshotQuery({
- queryKey,
- queryFn: () =>
- apiClient.get(
- "v1/admin/vector-storage/migrations",
- searchParams,
- ),
- path,
- });
-};
-
-export const useStartVectorMigration = () => {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (body: { collection: string; target_provider: VectorMigrationProvider }) =>
- apiClient.post("v1/admin/vector-storage/migrations", body),
- onSuccess: (job) => {
- queryClient.invalidateQueries({ queryKey: queryKeys.vectorMigrations({}) });
- queryClient.invalidateQueries({
- queryKey: queryKeys.vectorMigrations({ collection: job.collection_name }),
- });
- queryClient.invalidateQueries({ queryKey: queryKeys.collections.all() });
- queryClient.invalidateQueries({
- queryKey: queryKeys.collections.one({ name: job.collection_name }),
- });
- queryClient.invalidateQueries({ queryKey: queryKeys.vectorStorageOverview() });
- toast.success("Vector migration started");
- },
- onError: errorToast("Failed to start migration"),
- });
-};
-
-export const useDeleteVectorMigration = () => {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (job: VectorMigrationJob) =>
- apiClient.delete(
- `v1/admin/vector-storage/migrations/${encodeURIComponent(job.id)}`,
- ),
- onSuccess: (response, job) => {
- queryClient.invalidateQueries({ queryKey: queryKeys.vectorMigrations({}) });
- queryClient.invalidateQueries({
- queryKey: queryKeys.vectorMigrations({ collection: job.collection_name }),
- });
- queryClient.invalidateQueries({ queryKey: queryKeys.collections.all() });
- queryClient.invalidateQueries({
- queryKey: queryKeys.collections.one({ name: job.collection_name }),
- });
- queryClient.invalidateQueries({ queryKey: queryKeys.vectorStorageOverview() });
- toast.success(response.message ?? "Vector migration deleted");
- },
- onError: errorToast("Failed to delete migration"),
- });
-};
diff --git a/app/src/lib/query-keys.ts b/app/src/lib/query-keys.ts
index 0c4f39f4..5f17e48c 100644
--- a/app/src/lib/query-keys.ts
+++ b/app/src/lib/query-keys.ts
@@ -52,10 +52,6 @@ type AuditListParams = {
readonly resourceType?: string;
};
-type VectorMigrationsParams = {
- readonly collection?: string;
-};
-
export const queryKeys = {
auth: {
all: () => ["auth"] as const,
@@ -64,9 +60,6 @@ export const queryKeys = {
},
apiKeys: () => ["api-keys"] as const,
backups: () => ["backups"] as const,
- vectorStorageOverview: () => ["vector-storage", "overview"] as const,
- vectorMigrations: ({ collection }: VectorMigrationsParams = {}) =>
- ["vector-migrations", { collection: collection ?? "all" }] as const,
access: {
logs: (filters: Record) => ["access", "logs", filters] as const,
overview: ({ windowDays }: WindowDaysParams) => ["access", "overview", { windowDays }] as const,
diff --git a/app/src/routeTree.gen.ts b/app/src/routeTree.gen.ts
index 4dc40d53..2908d41d 100644
--- a/app/src/routeTree.gen.ts
+++ b/app/src/routeTree.gen.ts
@@ -9,7 +9,6 @@ import { Route as DashboardRouteImport } from "./routes/_dashboard";
import { Route as AuthRouteImport } from "./routes/_auth";
import { Route as IndexRouteImport } from "./routes/index";
import { Route as DashboardWebhooksRouteImport } from "./routes/_dashboard.webhooks";
-import { Route as DashboardVectorStorageRouteImport } from "./routes/_dashboard.vector-storage";
import { Route as DashboardUsageRouteImport } from "./routes/_dashboard.usage";
import { Route as DashboardSettingsRouteImport } from "./routes/_dashboard.settings";
import { Route as DashboardOverviewRouteImport } from "./routes/_dashboard.overview";
@@ -54,11 +53,6 @@ const DashboardWebhooksRoute = DashboardWebhooksRouteImport.update({
path: "/webhooks",
getParentRoute: () => DashboardRoute,
} as any);
-const DashboardVectorStorageRoute = DashboardVectorStorageRouteImport.update({
- id: "/vector-storage",
- path: "/vector-storage",
- getParentRoute: () => DashboardRoute,
-} as any);
const DashboardUsageRoute = DashboardUsageRouteImport.update({
id: "/usage",
path: "/usage",
@@ -212,7 +206,6 @@ export interface FileRoutesByFullPath {
"/overview": typeof DashboardOverviewRoute;
"/settings": typeof DashboardSettingsRoute;
"/usage": typeof DashboardUsageRoute;
- "/vector-storage": typeof DashboardVectorStorageRoute;
"/webhooks": typeof DashboardWebhooksRoute;
"/collections/$name": typeof DashboardCollectionsNameRouteWithChildren;
"/collections/": typeof DashboardCollectionsIndexRoute;
@@ -242,7 +235,6 @@ export interface FileRoutesByTo {
"/overview": typeof DashboardOverviewRoute;
"/settings": typeof DashboardSettingsRoute;
"/usage": typeof DashboardUsageRoute;
- "/vector-storage": typeof DashboardVectorStorageRoute;
"/webhooks": typeof DashboardWebhooksRoute;
"/collections": typeof DashboardCollectionsIndexRoute;
"/collections/$name/search": typeof DashboardCollectionsNameSearchRoute;
@@ -273,7 +265,6 @@ export interface FileRoutesById {
"/_dashboard/overview": typeof DashboardOverviewRoute;
"/_dashboard/settings": typeof DashboardSettingsRoute;
"/_dashboard/usage": typeof DashboardUsageRoute;
- "/_dashboard/vector-storage": typeof DashboardVectorStorageRoute;
"/_dashboard/webhooks": typeof DashboardWebhooksRoute;
"/_dashboard/collections/$name": typeof DashboardCollectionsNameRouteWithChildren;
"/_dashboard/collections/": typeof DashboardCollectionsIndexRoute;
@@ -305,7 +296,6 @@ export interface FileRouteTypes {
| "/overview"
| "/settings"
| "/usage"
- | "/vector-storage"
| "/webhooks"
| "/collections/$name"
| "/collections/"
@@ -335,7 +325,6 @@ export interface FileRouteTypes {
| "/overview"
| "/settings"
| "/usage"
- | "/vector-storage"
| "/webhooks"
| "/collections"
| "/collections/$name/search"
@@ -365,7 +354,6 @@ export interface FileRouteTypes {
| "/_dashboard/overview"
| "/_dashboard/settings"
| "/_dashboard/usage"
- | "/_dashboard/vector-storage"
| "/_dashboard/webhooks"
| "/_dashboard/collections/$name"
| "/_dashboard/collections/"
@@ -415,13 +403,6 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof DashboardWebhooksRouteImport;
parentRoute: typeof DashboardRoute;
};
- "/_dashboard/vector-storage": {
- id: "/_dashboard/vector-storage";
- path: "/vector-storage";
- fullPath: "/vector-storage";
- preLoaderRoute: typeof DashboardVectorStorageRouteImport;
- parentRoute: typeof DashboardRoute;
- };
"/_dashboard/usage": {
id: "/_dashboard/usage";
path: "/usage";
@@ -672,7 +653,6 @@ interface DashboardRouteChildren {
DashboardOverviewRoute: typeof DashboardOverviewRoute;
DashboardSettingsRoute: typeof DashboardSettingsRoute;
DashboardUsageRoute: typeof DashboardUsageRoute;
- DashboardVectorStorageRoute: typeof DashboardVectorStorageRoute;
DashboardWebhooksRoute: typeof DashboardWebhooksRoute;
DashboardCollectionsNameRoute: typeof DashboardCollectionsNameRouteWithChildren;
DashboardCollectionsIndexRoute: typeof DashboardCollectionsIndexRoute;
@@ -692,7 +672,6 @@ const DashboardRouteChildren: DashboardRouteChildren = {
DashboardOverviewRoute: DashboardOverviewRoute,
DashboardSettingsRoute: DashboardSettingsRoute,
DashboardUsageRoute: DashboardUsageRoute,
- DashboardVectorStorageRoute: DashboardVectorStorageRoute,
DashboardWebhooksRoute: DashboardWebhooksRoute,
DashboardCollectionsNameRoute: DashboardCollectionsNameRouteWithChildren,
DashboardCollectionsIndexRoute: DashboardCollectionsIndexRoute,
diff --git a/app/src/routes/_dashboard.collections.$name.index.tsx b/app/src/routes/_dashboard.collections.$name.index.tsx
index 764cd11b..72bc04fc 100644
--- a/app/src/routes/_dashboard.collections.$name.index.tsx
+++ b/app/src/routes/_dashboard.collections.$name.index.tsx
@@ -40,10 +40,6 @@ const CollectionIndex = () => {
-
diff --git a/app/src/routes/_dashboard.collections.$name.settings.tsx b/app/src/routes/_dashboard.collections.$name.settings.tsx
index 95a4965d..7c263cf9 100644
--- a/app/src/routes/_dashboard.collections.$name.settings.tsx
+++ b/app/src/routes/_dashboard.collections.$name.settings.tsx
@@ -1,5 +1,5 @@
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
-import { ArrowRightLeft, KeyRound, Trash2, TriangleAlert } from "lucide-react";
+import { KeyRound, Trash2, TriangleAlert } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@@ -12,7 +12,6 @@ import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { decodeCollectionName } from "@/features/collections/use-collection-name";
-import { VectorMigrationModal } from "@/features/vector-storage/vector-migration-panel";
import {
useCollection,
useDeleteCollection,
@@ -57,7 +56,6 @@ const CollectionSettings = () => {
const [allowedTypes, setAllowedTypes] = useState>(new Set(ALL_FILE_TYPES));
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [confirmTruncateOpen, setConfirmTruncateOpen] = useState(false);
- const [migrationOpen, setMigrationOpen] = useState(false);
useCollectionSettingsDraft(collection, {
setAllowedTypes,
@@ -378,25 +376,6 @@ const CollectionSettings = () => {
-
-
-
-
- Vector migration
-
-
- Move this collection to another configured vector provider. Source vectors are deleted
- after a successful cutover.
-
-
-
-
-
-
-
@@ -417,12 +396,6 @@ const CollectionSettings = () => {
- setMigrationOpen(false)}
- open={migrationOpen}
- />
-
setConfirmTruncateOpen(false)}
diff --git a/app/src/routes/_dashboard.collections.$name.tsx b/app/src/routes/_dashboard.collections.$name.tsx
index ed20a519..6e03af3d 100644
--- a/app/src/routes/_dashboard.collections.$name.tsx
+++ b/app/src/routes/_dashboard.collections.$name.tsx
@@ -51,10 +51,6 @@ const CollectionLayout = () => {
collection && (
Model {collection.embedding_model}
-
- Storage{" "}
- {collection.vector_store_provider === "turbopuffer" ? "Turbopuffer" : "Qdrant"}
-
Dimensions {collection.dimension}d
{collection.reranking_enabled && (
rerank: {collection.reranking_model}
diff --git a/app/src/routes/_dashboard.collections.index.tsx b/app/src/routes/_dashboard.collections.index.tsx
index 1775399b..30940996 100644
--- a/app/src/routes/_dashboard.collections.index.tsx
+++ b/app/src/routes/_dashboard.collections.index.tsx
@@ -57,20 +57,6 @@ const CollectionsPage = () => {
),
},
- {
- header: "Provider",
- key: "provider",
- render: (c) => (
-
- {c.embedding_provider}
-
- ),
- },
- {
- header: "Storage",
- key: "storage",
- render: (c) =>
{storageLabel(c.vector_store_provider)},
- },
{
header: "Model",
key: "model",
@@ -184,6 +170,3 @@ const CollectionsPage = () => {
);
};
-
-const storageLabel = (provider: Collection["vector_store_provider"]) =>
- provider === "turbopuffer" ? "Turbopuffer" : "Qdrant";
diff --git a/app/src/routes/_dashboard.vector-storage.tsx b/app/src/routes/_dashboard.vector-storage.tsx
deleted file mode 100644
index 37375356..00000000
--- a/app/src/routes/_dashboard.vector-storage.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router";
-
-type VectorStorageSearch = {
- provider?: string;
-};
-
-export const Route = createFileRoute("/_dashboard/vector-storage")({
- validateSearch: (search: Record
): VectorStorageSearch => ({
- provider: typeof search.provider === "string" ? search.provider : undefined,
- }),
- component: lazyRouteComponent(() =>
- import("@/features/vector-storage/vector-storage-route").then((m) => ({
- default: m.VectorStorageRoute,
- })),
- ),
-});
diff --git a/app/src/types/bigrag-api/admin.ts b/app/src/types/bigrag-api/admin.ts
index 2b431b42..da02029e 100644
--- a/app/src/types/bigrag-api/admin.ts
+++ b/app/src/types/bigrag-api/admin.ts
@@ -101,10 +101,8 @@ export type ReadinessReport = {
version: string;
postgres: boolean;
postgres_error?: string;
- qdrant: boolean | null;
vector_store: boolean;
vector_store_error?: string;
- vector_store_provider: "per_collection";
redis: boolean;
redis_error?: string;
embedding: boolean;
diff --git a/app/src/types/bigrag-api/settings.ts b/app/src/types/bigrag-api/settings.ts
index 2ce268f4..adf2737c 100644
--- a/app/src/types/bigrag-api/settings.ts
+++ b/app/src/types/bigrag-api/settings.ts
@@ -69,31 +69,3 @@ export type BackupJobListResponse = {
total: number | null;
next_cursor: string | null;
};
-
-export type VectorMigrationProvider = "qdrant" | "turbopuffer";
-
-export type VectorMigrationJob = {
- id: string;
- collection_id: string | null;
- collection_name: string;
- source_provider: VectorMigrationProvider;
- target_provider: VectorMigrationProvider;
- status: "pending" | "running" | "canceling" | "succeeded" | "failed";
- phase: string;
- progress: number;
- copied_points: number;
- total_points: number | null;
- details: Record;
- error_message: string | null;
- created_by: string | null;
- started_at: string | null;
- completed_at: string | null;
- created_at: string;
- updated_at: string;
-};
-
-export type VectorMigrationJobListResponse = {
- jobs: VectorMigrationJob[];
- total: number | null;
- next_cursor: string | null;
-};
diff --git a/dev.sh b/dev.sh
index 8bac5a9b..467a7a19 100755
--- a/dev.sh
+++ b/dev.sh
@@ -122,19 +122,17 @@ if [ "$START_WEBSITE" = true ]; then
fi
if [ "$START_INFRA" = true ]; then
- echo -e "${CYAN}Starting Docker services (Postgres, Redis, Qdrant)...${NC}"
- if ! docker compose -f "$ROOT_DIR/docker-compose.yml" ps --status running --quiet postgres redis qdrant | grep -q .; then
+ echo -e "${CYAN}Starting Docker services (Postgres, Redis)...${NC}"
+ if ! docker compose -f "$ROOT_DIR/docker-compose.yml" ps --status running --quiet postgres redis | grep -q .; then
STARTED_INFRA=true
fi
- docker compose -f "$ROOT_DIR/docker-compose.yml" up postgres redis qdrant -d
+ docker compose -f "$ROOT_DIR/docker-compose.yml" up postgres redis -d
wait_for "Postgres" "docker exec bigrag-postgres pg_isready -U bigrag" 30
wait_for "Redis" "docker exec bigrag-redis redis-cli ping" 15
- wait_for "Qdrant" "curl -sf http://localhost:6333/healthz" 60
fi
DATABASE_URL="postgres://bigrag:bigrag@localhost:5432/bigrag?sslmode=disable"
-QDRANT_URL="http://localhost:6333"
REDIS_URL="redis://localhost:6379/0"
if [ "$START_BACKEND" = true ]; then
@@ -148,8 +146,11 @@ if [ "$START_BACKEND" = true ]; then
DEV_MASTER_KEY="${BIGRAG_MASTER_KEY:-Zm5VZ4vO8r0y3rVsT0xz7nxV_wP7u6-n5tB1GAlHZIw=}"
export BIGRAG_DATABASE_URL="$DATABASE_URL"
- export BIGRAG_QDRANT_URL="$QDRANT_URL"
export BIGRAG_REDIS_URL="$REDIS_URL"
+ export BIGRAG_TURBOPUFFER_API_KEY="${BIGRAG_TURBOPUFFER_API_KEY:-}"
+ export BIGRAG_TURBOPUFFER_REGION="${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}"
+ export BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX="${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}"
+ export BIGRAG_TURBOPUFFER_BASE_URL="${BIGRAG_TURBOPUFFER_BASE_URL:-}"
export BIGRAG_MASTER_KEY="$DEV_MASTER_KEY"
export BIGRAG_CORS_ORIGINS="${BIGRAG_CORS_ORIGINS:-[\"http://localhost:3000\"]}"
export PYTHONUNBUFFERED=1
@@ -186,7 +187,6 @@ echo -e "\n${GREEN}Services started:${NC}"
[ "$START_BACKEND" = true ] && echo -e " API Docs → http://localhost:4000/docs"
[ "$START_INFRA" = true ] && echo -e " Postgres → localhost:5432"
[ "$START_INFRA" = true ] && echo -e " Redis → localhost:6379"
-[ "$START_INFRA" = true ] && echo -e " Qdrant → localhost:6333"
[ "$START_WEBSITE" = true ] && echo -e " Website → http://localhost:3100"
echo -e "\n${YELLOW}Press Ctrl+C to stop all services.${NC}"
diff --git a/docker-compose.yml b/docker-compose.yml
index 1cd904a0..4fdce40c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,8 +1,11 @@
x-bigrag-env: &bigrag-env
BIGRAG_ENV: "${BIGRAG_ENV:-dev}"
BIGRAG_DATABASE_URL: "postgres://${POSTGRES_USER:-bigrag}:${POSTGRES_PASSWORD:-bigrag}@postgres:5432/${POSTGRES_DB:-bigrag}?sslmode=disable"
- BIGRAG_QDRANT_URL: http://qdrant:6333
BIGRAG_REDIS_URL: "${BIGRAG_REDIS_URL:-redis://redis:6379/0}"
+ BIGRAG_TURBOPUFFER_API_KEY: "${BIGRAG_TURBOPUFFER_API_KEY:-}"
+ BIGRAG_TURBOPUFFER_REGION: "${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}"
+ BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: "${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}"
+ BIGRAG_TURBOPUFFER_BASE_URL: "${BIGRAG_TURBOPUFFER_BASE_URL:-}"
BIGRAG_MASTER_KEY: "${BIGRAG_MASTER_KEY:-}"
BIGRAG_MASTER_KEY_PREVIOUS: '${BIGRAG_MASTER_KEY_PREVIOUS:-[]}'
BIGRAG_SESSION_COOKIE_SECURE: "${BIGRAG_SESSION_COOKIE_SECURE:-false}"
@@ -38,8 +41,6 @@ services:
condition: service_healthy
redis:
condition: service_healthy
- qdrant:
- condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 30s
@@ -66,8 +67,6 @@ services:
condition: service_healthy
redis:
condition: service_healthy
- qdrant:
- condition: service_started
healthcheck:
test:
[
@@ -145,22 +144,7 @@ services:
timeout: 5s
retries: 5
- qdrant:
- image: qdrant/qdrant:v1.17.1
- container_name: bigrag-qdrant
- restart: unless-stopped
- ports:
- - "127.0.0.1:6333:6333"
- - "127.0.0.1:6334:6334"
- volumes:
- - qdrant_data:/qdrant/storage
- deploy:
- resources:
- limits:
- memory: 2g
-
volumes:
bigrag_data:
postgres_data:
redis_data:
- qdrant_data:
diff --git a/sdks/python/src/bigrag/resources/admin/__init__.py b/sdks/python/src/bigrag/resources/admin/__init__.py
index 0d4c6472..da0090f5 100644
--- a/sdks/python/src/bigrag/resources/admin/__init__.py
+++ b/sdks/python/src/bigrag/resources/admin/__init__.py
@@ -15,7 +15,6 @@
from bigrag.resources.admin.realtime import AdminRealtimeResource
from bigrag.resources.admin.settings import AdminSettingsResource
from bigrag.resources.admin.users import AdminUsersResource
-from bigrag.resources.admin.vector_migrations import AdminVectorMigrationsResource
if TYPE_CHECKING:
from bigrag._core import BigRAGCore
@@ -32,7 +31,6 @@ class AdminResource:
mcp_servers: AdminMcpServersResource
realtime: AdminRealtimeResource
settings: AdminSettingsResource
- vector_migrations: AdminVectorMigrationsResource
def __init__(self, client: BigRAGCore) -> None:
self.users = AdminUsersResource(client)
@@ -45,7 +43,6 @@ def __init__(self, client: BigRAGCore) -> None:
self.mcp_servers = AdminMcpServersResource(client)
self.realtime = AdminRealtimeResource(client)
self.settings = AdminSettingsResource(client)
- self.vector_migrations = AdminVectorMigrationsResource(client)
__all__ = [
@@ -61,5 +58,4 @@ def __init__(self, client: BigRAGCore) -> None:
"AdminResource",
"AdminSettingsResource",
"AdminUsersResource",
- "AdminVectorMigrationsResource",
]
diff --git a/sdks/python/src/bigrag/resources/admin/realtime.py b/sdks/python/src/bigrag/resources/admin/realtime.py
index b9cec978..91740531 100644
--- a/sdks/python/src/bigrag/resources/admin/realtime.py
+++ b/sdks/python/src/bigrag/resources/admin/realtime.py
@@ -88,18 +88,6 @@ def backups(
"/v1/admin/realtime/backups", {"limit": limit, "offset": offset}
)
- def vector_migrations(
- self,
- *,
- collection: str | None = None,
- limit: int | None = None,
- offset: int | None = None,
- ) -> AsyncGenerator[AdminRealtimeEvent, None]:
- return self._stream(
- "/v1/admin/realtime/vector-migrations",
- {"collection": collection, "limit": limit, "offset": offset},
- )
-
def access_overview(
self, *, window_days: int | None = None
) -> AsyncGenerator[AdminRealtimeEvent, None]:
diff --git a/sdks/python/src/bigrag/resources/admin/vector_migrations.py b/sdks/python/src/bigrag/resources/admin/vector_migrations.py
deleted file mode 100644
index bf8e3715..00000000
--- a/sdks/python/src/bigrag/resources/admin/vector_migrations.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
-from urllib.parse import quote
-
-from bigrag.resources.admin._shared import _pagination
-from bigrag.types.admin import (
- VectorMigrationCreateBody,
- VectorMigrationJob,
- VectorMigrationJobListResponse,
-)
-from bigrag.types.common import StatusResponse
-
-if TYPE_CHECKING:
- from bigrag._core import BigRAGCore
-
-
-class AdminVectorMigrationsResource:
- def __init__(self, client: BigRAGCore) -> None:
- self._client = client
-
- async def list(
- self,
- *,
- collection: str | None = None,
- cursor: str | None = None,
- include_total: bool | None = None,
- limit: int | None = None,
- offset: int | None = None,
- ) -> VectorMigrationJobListResponse:
- params = _pagination(limit=limit, offset=offset)
- if collection is not None:
- params["collection"] = collection
- if cursor is not None:
- params["cursor"] = cursor
- if include_total is not None:
- params["include_total"] = "true" if include_total else "false"
- return await self._client._request(
- "GET", "/v1/admin/vector-storage/migrations", params=params
- )
-
- async def get(self, migration_id: str) -> VectorMigrationJob:
- return await self._client._request(
- "GET", f"/v1/admin/vector-storage/migrations/{quote(migration_id, safe='')}"
- )
-
- async def create(self, body: VectorMigrationCreateBody) -> VectorMigrationJob:
- return await self._client._request(
- "POST", "/v1/admin/vector-storage/migrations", json=body
- )
-
- async def delete(self, migration_id: str) -> StatusResponse:
- return await self._client._request(
- "DELETE",
- f"/v1/admin/vector-storage/migrations/{quote(migration_id, safe='')}",
- )
diff --git a/sdks/python/src/bigrag/types/__init__.py b/sdks/python/src/bigrag/types/__init__.py
index 97e9ec28..1ccbad5e 100644
--- a/sdks/python/src/bigrag/types/__init__.py
+++ b/sdks/python/src/bigrag/types/__init__.py
@@ -38,11 +38,6 @@
UpdateMcpServerBody,
UpdateUserBody,
UserListResponse,
- VectorMigrationCreateBody,
- VectorMigrationJob,
- VectorMigrationJobListResponse,
- VectorMigrationJobStatus,
- VectorMigrationProvider,
)
from bigrag.types.analytics import AnalyticsResponse, PeriodStats, TopQuery
from bigrag.types.auth import (
@@ -134,6 +129,7 @@
QueryResponse,
QueryResult,
QueryTimings,
+ SearchMode,
)
from bigrag.types.sse import ProgressEvent
from bigrag.types.usage import CollectionUsage, UsageResponse
@@ -201,11 +197,6 @@
"BackupCreateBody",
"BackupJob",
"BackupJobListResponse",
- "VectorMigrationProvider",
- "VectorMigrationJobStatus",
- "VectorMigrationCreateBody",
- "VectorMigrationJob",
- "VectorMigrationJobListResponse",
"AdminRealtimeEvent",
"AccessLogEntry",
"AccessLogListResponse",
@@ -258,6 +249,7 @@
"QueryBody",
"QueryResult",
"QueryTimings",
+ "SearchMode",
"QueryResponse",
"MultiQueryBody",
"MultiQueryResult",
diff --git a/sdks/python/src/bigrag/types/admin.py b/sdks/python/src/bigrag/types/admin.py
index 06bd6fb7..ed34879e 100644
--- a/sdks/python/src/bigrag/types/admin.py
+++ b/sdks/python/src/bigrag/types/admin.py
@@ -100,43 +100,6 @@ class BackupJobListResponse(TypedDict):
total: int
-VectorMigrationProvider = Literal["qdrant", "turbopuffer"]
-VectorMigrationJobStatus = Literal[
- "pending", "running", "canceling", "succeeded", "failed"
-]
-
-
-class VectorMigrationCreateBody(TypedDict):
- collection: str
- target_provider: VectorMigrationProvider
-
-
-class VectorMigrationJob(TypedDict):
- id: str
- collection_id: str | None
- collection_name: str
- source_provider: VectorMigrationProvider
- target_provider: VectorMigrationProvider
- status: VectorMigrationJobStatus
- phase: str
- progress: float
- copied_points: int
- total_points: int | None
- details: dict[str, Any]
- error_message: str | None
- created_by: str | None
- started_at: str | None
- completed_at: str | None
- created_at: str
- updated_at: str
-
-
-class VectorMigrationJobListResponse(TypedDict):
- jobs: list[VectorMigrationJob]
- total: int | None
- next_cursor: str | None
-
-
class AdminRealtimeEvent(TypedDict):
event: str
data: Any
diff --git a/sdks/python/src/bigrag/types/chat.py b/sdks/python/src/bigrag/types/chat.py
index 38ee5865..b329fc34 100644
--- a/sdks/python/src/bigrag/types/chat.py
+++ b/sdks/python/src/bigrag/types/chat.py
@@ -3,7 +3,7 @@
from typing import Any, NotRequired, TypedDict
from .documents import MultimodalElementRef
-from .query import QueryTimings
+from .query import QueryTimings, SearchMode
class ChatBody(TypedDict):
@@ -14,7 +14,7 @@ class ChatBody(TypedDict):
model: NotRequired[str]
temperature: NotRequired[float]
top_k: NotRequired[int]
- search_mode: NotRequired[str]
+ search_mode: NotRequired[SearchMode]
min_score: NotRequired[float | None]
rerank: NotRequired[bool | None]
filters: NotRequired[dict[str, Any] | None]
diff --git a/sdks/python/src/bigrag/types/collections.py b/sdks/python/src/bigrag/types/collections.py
index 2cb07b64..07f1dfb0 100644
--- a/sdks/python/src/bigrag/types/collections.py
+++ b/sdks/python/src/bigrag/types/collections.py
@@ -2,14 +2,14 @@
from typing import Any, NotRequired, TypedDict
+from .query import SearchMode
+
class Collection(TypedDict):
id: str
name: str
description: str
- embedding_provider: str
embedding_model: str
- vector_store_provider: str
dimension: int
chunk_size: int
chunk_overlap: int
@@ -27,7 +27,7 @@ class Collection(TypedDict):
multimodal_enrichment_enabled: bool
default_top_k: int
default_min_score: float | None
- default_search_mode: str
+ default_search_mode: SearchMode
metadata: dict[str, Any]
created_at: str
updated_at: str
@@ -50,9 +50,7 @@ class CollectionStatsResponse(TypedDict):
class CreateCollectionBody(TypedDict):
name: str
description: NotRequired[str]
- vector_store_provider: NotRequired[str]
embedding_preset_id: NotRequired[str]
- embedding_provider: NotRequired[str]
embedding_model: NotRequired[str]
embedding_api_key: NotRequired[str]
embedding_base_url: NotRequired[str]
@@ -71,7 +69,7 @@ class CreateCollectionBody(TypedDict):
multimodal_enrichment_enabled: NotRequired[bool]
default_top_k: NotRequired[int]
default_min_score: NotRequired[float]
- default_search_mode: NotRequired[str]
+ default_search_mode: NotRequired[SearchMode]
class UpdateCollectionBody(TypedDict, total=False):
@@ -85,6 +83,6 @@ class UpdateCollectionBody(TypedDict, total=False):
multimodal_enrichment_enabled: bool
default_top_k: int
default_min_score: float
- default_search_mode: str
+ default_search_mode: SearchMode
chunk_strategy: str
metadata_schema: dict[str, Any]
diff --git a/sdks/python/src/bigrag/types/common.py b/sdks/python/src/bigrag/types/common.py
index 40a21054..58a19631 100644
--- a/sdks/python/src/bigrag/types/common.py
+++ b/sdks/python/src/bigrag/types/common.py
@@ -18,8 +18,6 @@ class ReadinessResponse(TypedDict):
version: str
postgres: bool
vector_store: bool
- vector_store_provider: NotRequired[str]
- qdrant: NotRequired[bool | None]
redis: bool
embedding: NotRequired[bool]
embedding_source: NotRequired[str]
diff --git a/sdks/python/src/bigrag/types/evaluations.py b/sdks/python/src/bigrag/types/evaluations.py
index 655e41ca..d7cbe1b1 100644
--- a/sdks/python/src/bigrag/types/evaluations.py
+++ b/sdks/python/src/bigrag/types/evaluations.py
@@ -2,6 +2,8 @@
from typing import NotRequired, TypedDict
+from .query import SearchMode
+
class EvalCase(TypedDict):
query: str
@@ -13,7 +15,7 @@ class EvalBody(TypedDict):
collection: str
cases: list[EvalCase]
top_k: NotRequired[int]
- search_mode: NotRequired[str]
+ search_mode: NotRequired[SearchMode]
class EvalPerCase(TypedDict):
diff --git a/sdks/python/src/bigrag/types/query.py b/sdks/python/src/bigrag/types/query.py
index f707838a..e13e13c4 100644
--- a/sdks/python/src/bigrag/types/query.py
+++ b/sdks/python/src/bigrag/types/query.py
@@ -1,16 +1,18 @@
from __future__ import annotations
-from typing import Any, NotRequired, TypedDict
+from typing import Any, Literal, NotRequired, TypedDict
from .documents import MultimodalElementRef
+SearchMode = Literal["semantic", "keyword", "hybrid"]
+
class QueryBody(TypedDict):
query: str
top_k: NotRequired[int]
filters: NotRequired[dict[str, Any]]
min_score: NotRequired[float]
- search_mode: NotRequired[str]
+ search_mode: NotRequired[SearchMode]
rerank: NotRequired[bool]
multimodal: NotRequired[bool]
@@ -52,7 +54,7 @@ class MultiQueryBody(TypedDict):
top_k: NotRequired[int]
filters: NotRequired[dict[str, Any]]
min_score: NotRequired[float]
- search_mode: NotRequired[str]
+ search_mode: NotRequired[SearchMode]
rerank: NotRequired[bool]
multimodal: NotRequired[bool]
@@ -82,7 +84,7 @@ class BatchQueryItem(TypedDict):
top_k: NotRequired[int]
filters: NotRequired[dict[str, Any]]
min_score: NotRequired[float]
- search_mode: NotRequired[str]
+ search_mode: NotRequired[SearchMode]
rerank: NotRequired[bool]
multimodal: NotRequired[bool]
diff --git a/sdks/typescript/src/resources/admin/index.ts b/sdks/typescript/src/resources/admin/index.ts
index 3abcd8e8..0195cf4b 100644
--- a/sdks/typescript/src/resources/admin/index.ts
+++ b/sdks/typescript/src/resources/admin/index.ts
@@ -9,7 +9,6 @@ import { AdminMcpServersResource } from "./mcp_servers.js";
import { AdminRealtimeResource } from "./realtime.js";
import { AdminSettingsResource } from "./settings.js";
import { AdminUsersResource } from "./users.js";
-import { AdminVectorMigrationsResource } from "./vector_migrations.js";
export { AdminAccessResource } from "./access.js";
export { AdminApiKeysResource } from "./api_keys.js";
@@ -21,7 +20,6 @@ export { AdminMcpServersResource } from "./mcp_servers.js";
export { AdminRealtimeResource } from "./realtime.js";
export { AdminSettingsResource } from "./settings.js";
export { AdminUsersResource } from "./users.js";
-export { AdminVectorMigrationsResource } from "./vector_migrations.js";
export class AdminResource {
readonly users: AdminUsersResource;
@@ -34,7 +32,6 @@ export class AdminResource {
readonly mcpServers: AdminMcpServersResource;
readonly realtime: AdminRealtimeResource;
readonly settings: AdminSettingsResource;
- readonly vectorMigrations: AdminVectorMigrationsResource;
constructor(client: RequestClient) {
this.users = new AdminUsersResource(client);
@@ -47,6 +44,5 @@ export class AdminResource {
this.mcpServers = new AdminMcpServersResource(client);
this.realtime = new AdminRealtimeResource(client);
this.settings = new AdminSettingsResource(client);
- this.vectorMigrations = new AdminVectorMigrationsResource(client);
}
}
diff --git a/sdks/typescript/src/resources/admin/realtime.ts b/sdks/typescript/src/resources/admin/realtime.ts
index f90cdedd..457c4170 100644
--- a/sdks/typescript/src/resources/admin/realtime.ts
+++ b/sdks/typescript/src/resources/admin/realtime.ts
@@ -17,7 +17,6 @@ import type {
ReadinessResponse,
UploadSession,
UsageResponse,
- VectorMigrationJobListResponse,
} from "../../types/index.js";
export class AdminRealtimeResource {
@@ -102,16 +101,6 @@ export class AdminRealtimeResource {
return this._stream("/v1/admin/realtime/backups", options);
}
- vectorMigrations(
- options: { collection?: string; limit?: number; offset?: number } = {},
- ): AsyncGenerator> {
- return this._stream("/v1/admin/realtime/vector-migrations", {
- collection: options.collection,
- limit: options.limit,
- offset: options.offset,
- });
- }
-
accessOverview(
options: { windowDays?: number } = {},
): AsyncGenerator> {
diff --git a/sdks/typescript/src/resources/admin/vector_migrations.ts b/sdks/typescript/src/resources/admin/vector_migrations.ts
deleted file mode 100644
index 7fdda41a..00000000
--- a/sdks/typescript/src/resources/admin/vector_migrations.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { RequestClient } from "../../core.js";
-import type {
- StatusResponse,
- VectorMigrationCreateBody,
- VectorMigrationJob,
- VectorMigrationJobListResponse,
-} from "../../types/index.js";
-import { pagination } from "./_shared.js";
-
-type VectorMigrationListOptions = {
- collection?: string;
- cursor?: string;
- includeTotal?: boolean;
- limit?: number;
- offset?: number;
-};
-
-export class AdminVectorMigrationsResource {
- constructor(private readonly _client: RequestClient) {}
-
- list(options: VectorMigrationListOptions = {}): Promise {
- return this._client._request("GET", "/v1/admin/vector-storage/migrations", {
- params: {
- ...pagination(options),
- ...(options.collection ? { collection: options.collection } : {}),
- ...(options.cursor ? { cursor: options.cursor } : {}),
- ...(options.includeTotal === undefined
- ? {}
- : { include_total: String(options.includeTotal) }),
- },
- });
- }
-
- get(migrationId: string): Promise {
- return this._client._request(
- "GET",
- `/v1/admin/vector-storage/migrations/${encodeURIComponent(migrationId)}`,
- );
- }
-
- create(body: VectorMigrationCreateBody): Promise {
- return this._client._request("POST", "/v1/admin/vector-storage/migrations", {
- json: body,
- });
- }
-
- delete(migrationId: string): Promise {
- return this._client._request(
- "DELETE",
- `/v1/admin/vector-storage/migrations/${encodeURIComponent(migrationId)}`,
- );
- }
-}
diff --git a/sdks/typescript/src/types/admin.ts b/sdks/typescript/src/types/admin.ts
index 3fba3dfb..1014ca28 100644
--- a/sdks/typescript/src/types/admin.ts
+++ b/sdks/typescript/src/types/admin.ts
@@ -93,40 +93,6 @@ export interface BackupJobListResponse {
total: number;
}
-export type VectorMigrationProvider = "qdrant" | "turbopuffer";
-export type VectorMigrationJobStatus = "pending" | "running" | "canceling" | "succeeded" | "failed";
-
-export interface VectorMigrationCreateBody {
- collection: string;
- target_provider: VectorMigrationProvider;
-}
-
-export interface VectorMigrationJob {
- id: string;
- collection_id: string | null;
- collection_name: string;
- source_provider: VectorMigrationProvider;
- target_provider: VectorMigrationProvider;
- status: VectorMigrationJobStatus;
- phase: string;
- progress: number;
- copied_points: number;
- total_points: number | null;
- details: Record;
- error_message: string | null;
- created_by: string | null;
- started_at: string | null;
- completed_at: string | null;
- created_at: string;
- updated_at: string;
-}
-
-export interface VectorMigrationJobListResponse {
- jobs: VectorMigrationJob[];
- total: number | null;
- next_cursor: string | null;
-}
-
export interface AdminRealtimeSnapshot {
event: "snapshot";
data: {
diff --git a/sdks/typescript/src/types/collections.ts b/sdks/typescript/src/types/collections.ts
index b488d5f0..e86a0342 100644
--- a/sdks/typescript/src/types/collections.ts
+++ b/sdks/typescript/src/types/collections.ts
@@ -2,9 +2,7 @@ export interface Collection {
id: string;
name: string;
description: string;
- embedding_provider: string;
embedding_model: string;
- vector_store_provider: "qdrant" | "turbopuffer";
dimension: number;
chunk_size: number;
chunk_overlap: number;
@@ -52,9 +50,7 @@ export interface CollectionStatsResponse {
export interface CreateCollectionBody {
name: string;
description?: string;
- vector_store_provider?: "qdrant" | "turbopuffer";
embedding_preset_id?: string;
- embedding_provider?: string;
embedding_model?: string;
embedding_api_key?: string;
embedding_base_url?: string;
diff --git a/sdks/typescript/src/types/common.ts b/sdks/typescript/src/types/common.ts
index 4793f26e..14e42601 100644
--- a/sdks/typescript/src/types/common.ts
+++ b/sdks/typescript/src/types/common.ts
@@ -13,8 +13,6 @@ export interface ReadinessResponse {
version: string;
postgres: boolean;
vector_store: boolean;
- vector_store_provider?: string;
- qdrant?: boolean | null;
redis: boolean;
embedding?: boolean;
embedding_source?: string;
From 07e991d455eae66d61fc4dacc5f77d06bce08031 Mon Sep 17 00:00:00 2001
From: Bigint <69431456+bigint@users.noreply.github.com>
Date: Thu, 21 May 2026 22:58:15 +0530
Subject: [PATCH 2/9] Switch e2e stack to turbopuffer fakes
---
e2e/Makefile | 2 +-
e2e/README.md | 16 +-
e2e/conftest.py | 38 ++--
e2e/docker-compose.e2e.yml | 28 ++-
e2e/fixtures/documents/sample.md | 2 +-
e2e/fixtures/documents/sample.txt | 2 +-
e2e/stubs/fake_turbopuffer/Dockerfile | 12 ++
e2e/stubs/fake_turbopuffer/app/__init__.py | 1 +
e2e/stubs/fake_turbopuffer/app/main.py | 184 +++++++++++++++++++
e2e/stubs/fake_turbopuffer/requirements.txt | 2 +
e2e/tests/_helpers.py | 6 +-
e2e/tests/api/test_collections.py | 1 -
e2e/tests/api/test_errors.py | 4 +-
e2e/tests/api/test_health.py | 4 +-
e2e/tests/api/test_realtime_sse.py | 15 --
e2e/tests/api/test_settings.py | 33 +++-
e2e/tests/api/test_vector_storage.py | 95 +---------
e2e/tests/sdk_python/test_collections.py | 2 -
e2e/tests/sdk_typescript/auth.test.ts | 1 -
e2e/tests/sdk_typescript/collections.test.ts | 2 -
e2e/tests/sdk_typescript/helpers.ts | 3 -
21 files changed, 305 insertions(+), 148 deletions(-)
create mode 100644 e2e/stubs/fake_turbopuffer/Dockerfile
create mode 100644 e2e/stubs/fake_turbopuffer/app/__init__.py
create mode 100644 e2e/stubs/fake_turbopuffer/app/main.py
create mode 100644 e2e/stubs/fake_turbopuffer/requirements.txt
diff --git a/e2e/Makefile b/e2e/Makefile
index d43960cb..13320a15 100644
--- a/e2e/Makefile
+++ b/e2e/Makefile
@@ -3,7 +3,7 @@
E2E_PROJECT ?= bigrag-e2e
COMPOSE = docker compose -p $(E2E_PROJECT) -f ../docker-compose.yml -f docker-compose.e2e.yml
API_BASE ?= http://localhost:4000
-E2E_SERVICES = bigrag-api bigrag-worker fake-openai webhook-sink minio
+E2E_SERVICES = bigrag-api bigrag-worker fake-openai fake-turbopuffer webhook-sink minio
up:
$(COMPOSE) up -d --build $(E2E_SERVICES)
diff --git a/e2e/README.md b/e2e/README.md
index 95d5ee52..d20c3abf 100644
--- a/e2e/README.md
+++ b/e2e/README.md
@@ -54,15 +54,15 @@ used by `./dev.sh`.
|
host:4000 v host:9001/9002/9003
+-------------+--------+ +-----------------------------+
- | bigrag-api | | fake-openai | fake-gdrive | |
+ | bigrag-api | | fake-openai | fake-turbo | |
| bigrag-worker | | webhook-sink |
+--+---------+---------+ +-----------------------------+
| | ^ ^ ^
v v | | |
- postgres redis qdrant +------------+---------+----+
+ postgres redis turbopuffer +--------+---------+----+
(bigrag-api routes embeddings, chat,
- Google Drive connector, and webhook
- deliveries at these local fakes)
+ vectors, and webhook deliveries at
+ these local fakes)
```
The fakes live in `stubs/`:
@@ -70,8 +70,8 @@ The fakes live in `stubs/`:
- `fake-openai` — OpenAI-compatible `/v1/embeddings`, `/v1/chat/completions`
(non-stream + SSE), `/v1/models`. Deterministic embeddings via
sha256-seeded numpy RNG; canned chat responses.
-- `fake-gdrive` — Mock OAuth (`/o/oauth2/*`) + Drive (`/drive/v3/*`) for the
- Google Drive connector tests.
+- `fake-turbopuffer` — Turbopuffer-compatible namespace writes, vector query,
+ fake BM25 keyword query, and hybrid-friendly rows.
- `webhook-sink` — Records every incoming webhook delivery; tests poll
`/received?label=...` to assert.
@@ -113,8 +113,8 @@ All Python suites share fixtures from `e2e/conftest.py`:
See `e2e/conftest.py` and `e2e/tests/_helpers.py` for the full contract.
-Collection names are short — `e2e_<8 hex>` — so they fit Qdrant's collection
-name limit. Each fixture cleans up in teardown so the suite is safe under
+Collection names are short — `e2e_<8 hex>` — so generated namespace names stay
+compact. Each fixture cleans up in teardown so the suite is safe under
`pytest-xdist -n auto`.
## Coverage
diff --git a/e2e/conftest.py b/e2e/conftest.py
index c10435e3..84d25213 100644
--- a/e2e/conftest.py
+++ b/e2e/conftest.py
@@ -35,9 +35,11 @@
API_BASE = os.environ.get("BIGRAG_E2E_API_BASE", "http://localhost:4000")
FAKE_OPENAI_BASE = os.environ.get("BIGRAG_E2E_FAKE_OPENAI", "http://localhost:9001")
+FAKE_TURBOPUFFER_BASE = os.environ.get("BIGRAG_E2E_FAKE_TURBOPUFFER", "http://localhost:9002")
WEBHOOK_SINK_BASE = os.environ.get("BIGRAG_E2E_WEBHOOK_SINK", "http://localhost:9003")
FAKE_OPENAI_INTERNAL_BASE = "http://fake-openai:9001"
+FAKE_TURBOPUFFER_INTERNAL_BASE = "http://fake-turbopuffer:9002"
WEBHOOK_SINK_INTERNAL_BASE = "http://webhook-sink:9003"
ADMIN_EMAIL = "e2e-admin@example.com"
@@ -64,6 +66,11 @@ def fake_openai_base() -> str:
return FAKE_OPENAI_INTERNAL_BASE
+@pytest.fixture(scope="session")
+def fake_turbopuffer_base() -> str:
+ return FAKE_TURBOPUFFER_INTERNAL_BASE
+
+
# ---------------------------------------------------------------------------
# Unauthenticated client (function-scoped)
# ---------------------------------------------------------------------------
@@ -137,15 +144,26 @@ async def _bootstrap_e2e_runtime_settings() -> None:
) as client:
for k, v in cookies.items():
client.cookies.set(k, v)
+ settings_resp = await client.get("/v1/admin/settings")
+ if settings_resp.status_code != 200:
+ return
+ spec_keys = {spec["key"] for spec in settings_resp.json().get("specs", [])}
+ values: dict[str, Any] = {
+ "allow_private_embedding_base_urls": True,
+ "allow_private_chat_base_urls": True,
+ "allow_local_webhooks": True,
+ }
+ if "turbopuffer_api_key" in spec_keys:
+ values["turbopuffer_api_key"] = "e2e-fake-turbopuffer-key"
+ if "turbopuffer_region" in spec_keys:
+ values["turbopuffer_region"] = "e2e"
+ if "turbopuffer_namespace_prefix" in spec_keys:
+ values["turbopuffer_namespace_prefix"] = "e2e_"
+ if "turbopuffer_base_url" in spec_keys:
+ values["turbopuffer_base_url"] = FAKE_TURBOPUFFER_INTERNAL_BASE
resp = await client.put(
"/v1/admin/settings",
- json={
- "values": {
- "allow_private_embedding_base_urls": True,
- "allow_private_chat_base_urls": True,
- "allow_local_webhooks": True,
- }
- },
+ json={"values": values},
)
if resp.status_code not in (200, 204):
return
@@ -389,9 +407,7 @@ async def collection(
The instance is configured with embedding credentials at deploy time
(``BIGRAG_EMBEDDING_*`` env vars point at fake-openai), so we only
need to supply the bare minimum here. To override, pass any
- ``CreateCollectionRequest`` field as a kwarg::
-
- coll = await collection(dimension=384, vector_store_provider="qdrant")
+ ``CreateCollectionRequest`` field as a kwarg.
"""
created_names: list[str] = []
@@ -400,7 +416,6 @@ async def _create(
name: str | None = None,
description: str = "e2e fixture collection",
dimension: int | None = 1536,
- vector_store_provider: str = "qdrant",
chunk_size: int = 512,
chunk_overlap: int = 50,
chunk_strategy: str = "paragraph",
@@ -423,7 +438,6 @@ async def _create(
body: dict[str, Any] = {
"name": name or unique_name("e2e"),
"description": description,
- "vector_store_provider": vector_store_provider,
"chunk_size": chunk_size,
"chunk_overlap": chunk_overlap,
"chunk_strategy": chunk_strategy,
diff --git a/e2e/docker-compose.e2e.yml b/e2e/docker-compose.e2e.yml
index 67633052..0c9b0b20 100644
--- a/e2e/docker-compose.e2e.yml
+++ b/e2e/docker-compose.e2e.yml
@@ -24,6 +24,10 @@ services:
BIGRAG_CHAT_BASE_URL: http://fake-openai:9001/v1
BIGRAG_CHAT_API_KEY: e2e-fake-key
BIGRAG_ALLOW_PRIVATE_CHAT_BASE_URLS: "true"
+ BIGRAG_TURBOPUFFER_API_KEY: e2e-fake-turbopuffer-key
+ BIGRAG_TURBOPUFFER_REGION: e2e
+ BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: e2e_
+ BIGRAG_TURBOPUFFER_BASE_URL: http://fake-turbopuffer:9002
BIGRAG_ALLOW_LOCAL_WEBHOOKS: "true"
BIGRAG_BACKUP_S3_BUCKET: bigrag-backups
BIGRAG_BACKUP_S3_ENDPOINT_URL: http://minio:9000
@@ -36,10 +40,10 @@ services:
condition: service_healthy
redis:
condition: service_healthy
- qdrant:
- condition: service_started
fake-openai:
condition: service_healthy
+ fake-turbopuffer:
+ condition: service_healthy
bigrag-worker:
command:
@@ -64,6 +68,10 @@ services:
BIGRAG_CHAT_BASE_URL: http://fake-openai:9001/v1
BIGRAG_CHAT_API_KEY: e2e-fake-key
BIGRAG_ALLOW_PRIVATE_CHAT_BASE_URLS: "true"
+ BIGRAG_TURBOPUFFER_API_KEY: e2e-fake-turbopuffer-key
+ BIGRAG_TURBOPUFFER_REGION: e2e
+ BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: e2e_
+ BIGRAG_TURBOPUFFER_BASE_URL: http://fake-turbopuffer:9002
BIGRAG_ALLOW_LOCAL_WEBHOOKS: "true"
depends_on:
bigrag-api:
@@ -72,10 +80,10 @@ services:
condition: service_healthy
redis:
condition: service_healthy
- qdrant:
- condition: service_started
fake-openai:
condition: service_healthy
+ fake-turbopuffer:
+ condition: service_healthy
fake-openai:
build: ./e2e/stubs/fake_openai
@@ -89,6 +97,18 @@ services:
timeout: 3s
retries: 10
+ fake-turbopuffer:
+ build: ./e2e/stubs/fake_turbopuffer
+ container_name: bigrag-e2e-fake-turbopuffer
+ restart: unless-stopped
+ ports:
+ - "127.0.0.1:9002:9002"
+ healthcheck:
+ test: ["CMD-SHELL", "python -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen(\"http://localhost:9002/health\").status==200 else 1)'"]
+ interval: 5s
+ timeout: 3s
+ retries: 10
+
webhook-sink:
build: ./e2e/stubs/webhook_sink
container_name: bigrag-e2e-webhook-sink
diff --git a/e2e/fixtures/documents/sample.md b/e2e/fixtures/documents/sample.md
index 6bf98a75..b7797c9d 100644
--- a/e2e/fixtures/documents/sample.md
+++ b/e2e/fixtures/documents/sample.md
@@ -15,7 +15,7 @@ through CSRF, scope, and audit middleware.
2. Conversion service (Docling) extracts text from PDF/DOCX/HTML/PNG
3. Chunker splits the document by `chunk_strategy`
4. Embedding worker batches chunks and embeds via the configured provider
-5. Vectors and payloads are upserted into Qdrant or Turbopuffer
+5. Vectors and payloads are upserted into Turbopuffer
## Retrieval
diff --git a/e2e/fixtures/documents/sample.txt b/e2e/fixtures/documents/sample.txt
index 78431b58..a5f7b574 100644
--- a/e2e/fixtures/documents/sample.txt
+++ b/e2e/fixtures/documents/sample.txt
@@ -2,6 +2,6 @@ Acme Corp Overview
Acme Corp was founded in 2017 in Singapore by a team of three engineers focused on shipping reliable RAG infrastructure for builders. The company now employs 47 people across Singapore, Bangalore, and Berlin, and reported $12.4 million in annual recurring revenue in fiscal year 2026.
-The flagship product is the Acme Knowledge Platform, a self-hostable retrieval-augmented-generation stack that ingests documents from Google Drive, Notion, S3, and arbitrary HTTP sources, chunks them with configurable strategies, and indexes them in Qdrant or Turbopuffer. Customers include teams at fictional firms Globex, Initech, and Soylent.
+The flagship product is the Acme Knowledge Platform, a self-hostable retrieval-augmented-generation stack that ingests documents from Google Drive, Notion, S3, and arbitrary HTTP sources, chunks them with configurable strategies, and indexes them in Turbopuffer. Customers include teams at fictional firms Globex, Initech, and Soylent.
The Acme support guarantee promises a four-hour response time for production incidents and a 99.95% uptime SLA on the managed control plane. The CEO is Jordan Park; the CTO is Priya Iyer; the head of customer success is Marco Vidal. The company's office is located at 1 Marina Boulevard, Singapore 018989.
diff --git a/e2e/stubs/fake_turbopuffer/Dockerfile b/e2e/stubs/fake_turbopuffer/Dockerfile
new file mode 100644
index 00000000..3b814558
--- /dev/null
+++ b/e2e/stubs/fake_turbopuffer/Dockerfile
@@ -0,0 +1,12 @@
+FROM python:3.12-slim
+
+WORKDIR /srv
+
+COPY requirements.txt /srv/requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY app /srv/app
+
+EXPOSE 9002
+
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "9002"]
diff --git a/e2e/stubs/fake_turbopuffer/app/__init__.py b/e2e/stubs/fake_turbopuffer/app/__init__.py
new file mode 100644
index 00000000..9d48db4f
--- /dev/null
+++ b/e2e/stubs/fake_turbopuffer/app/__init__.py
@@ -0,0 +1 @@
+from __future__ import annotations
diff --git a/e2e/stubs/fake_turbopuffer/app/main.py b/e2e/stubs/fake_turbopuffer/app/main.py
new file mode 100644
index 00000000..29018e62
--- /dev/null
+++ b/e2e/stubs/fake_turbopuffer/app/main.py
@@ -0,0 +1,184 @@
+from __future__ import annotations
+
+import math
+import re
+from copy import deepcopy
+from typing import Any
+
+from fastapi import FastAPI, Request
+from fastapi.responses import JSONResponse
+
+app = FastAPI(title="fake-turbopuffer", version="0.1.0")
+
+_namespaces: dict[str, dict[str, dict[str, Any]]] = {}
+
+
+@app.get("/health")
+async def health() -> dict[str, str]:
+ return {"status": "ok"}
+
+
+@app.get("/v1/namespaces")
+async def list_namespaces() -> dict[str, list[dict[str, str]]]:
+ return {"namespaces": [{"id": name, "name": name} for name in sorted(_namespaces)]}
+
+
+@app.delete("/v2/namespaces/{namespace}")
+async def delete_namespace(namespace: str) -> JSONResponse:
+ if namespace not in _namespaces:
+ return JSONResponse({"error": "not found"}, status_code=404)
+ del _namespaces[namespace]
+ return JSONResponse({"status": "ok"})
+
+
+@app.post("/v2/namespaces/{namespace}")
+async def write_namespace(namespace: str, request: Request) -> dict[str, Any]:
+ body = await request.json()
+ rows = _namespaces.setdefault(namespace, {})
+ affected = 0
+ for row in body.get("upsert_rows") or []:
+ row_id = str(row["id"])
+ rows[row_id] = deepcopy(row)
+ affected += 1
+ for row_id in body.get("deletes") or []:
+ if rows.pop(str(row_id), None) is not None:
+ affected += 1
+ delete_filter = body.get("delete_by_filter")
+ if delete_filter is not None:
+ delete_ids = [row_id for row_id, row in rows.items() if _matches(row, delete_filter)]
+ for row_id in delete_ids:
+ rows.pop(row_id, None)
+ affected += len(delete_ids)
+ return {"status": "ok", "rows_affected": affected}
+
+
+@app.post("/v2/namespaces/{namespace}/query")
+async def query_namespace(namespace: str, request: Request) -> dict[str, list[dict[str, Any]]]:
+ body = await request.json()
+ rows = [
+ deepcopy(row)
+ for row in _namespaces.get(namespace, {}).values()
+ if _matches(row, body.get("filters"))
+ ]
+ ranked = _rank(rows, body.get("rank_by"))
+ total = _limit(body, default=len(ranked))
+ return {"rows": [_project(row, body) for row in ranked[:total]]}
+
+
+def _rank(rows: list[dict[str, Any]], rank_by: Any) -> list[dict[str, Any]]:
+ if not isinstance(rank_by, list) or len(rank_by) < 2:
+ return sorted(rows, key=lambda row: str(row.get("id", "")))
+ mode = str(rank_by[1]).lower()
+ field = str(rank_by[0])
+ if field == "id" and mode == "asc":
+ return sorted(rows, key=lambda row: str(row.get("id", "")))
+ if mode == "ann":
+ query_vector = rank_by[2] if len(rank_by) > 2 else []
+ for row in rows:
+ score = _cosine(row.get(field) or [], query_vector)
+ row["$dist"] = max(0.0, 1.0 - score)
+ row["$score"] = score
+ return sorted(rows, key=lambda row: float(row.get("$dist", 1.0)))
+ if mode in {"bm25", "hybrid"}:
+ query = str(rank_by[2] if len(rank_by) > 2 else "")
+ for row in rows:
+ score = _text_score(str(row.get(field, "")), query)
+ if mode == "hybrid":
+ score += max(0.0, _cosine(row.get("vector") or [], _vector_hint(rank_by))) * 0.25
+ row["$score"] = score
+ row["$dist"] = max(0.0, 1.0 - min(score, 1.0))
+ return sorted(rows, key=lambda row: float(row.get("$score", 0.0)), reverse=True)
+ return sorted(rows, key=lambda row: str(row.get("id", "")))
+
+
+def _vector_hint(rank_by: list[Any]) -> list[float]:
+ for item in rank_by:
+ if isinstance(item, list) and all(isinstance(value, (int, float)) for value in item):
+ return [float(value) for value in item]
+ return []
+
+
+def _text_score(text: str, query: str) -> float:
+ words = re.findall(r"[a-z0-9]+", text.lower())
+ terms = re.findall(r"[a-z0-9]+", query.lower())
+ if not words or not terms:
+ return 0.0
+ counts = {word: words.count(word) for word in set(words)}
+ hits = sum(counts.get(term, 0) for term in terms)
+ coverage = len({term for term in terms if term in counts}) / max(1, len(set(terms)))
+ return round(hits / len(words) + coverage, 6)
+
+
+def _cosine(left: list[Any], right: list[Any]) -> float:
+ if not left or not right:
+ return 0.0
+ size = min(len(left), len(right))
+ a = [float(value) for value in left[:size]]
+ b = [float(value) for value in right[:size]]
+ dot = sum(x * y for x, y in zip(a, b, strict=False))
+ norm_a = math.sqrt(sum(x * x for x in a))
+ norm_b = math.sqrt(sum(y * y for y in b))
+ if norm_a == 0.0 or norm_b == 0.0:
+ return 0.0
+ return dot / (norm_a * norm_b)
+
+
+def _limit(body: dict[str, Any], *, default: int) -> int:
+ if isinstance(body.get("top_k"), int):
+ return max(0, int(body["top_k"]))
+ limit = body.get("limit")
+ if isinstance(limit, dict) and isinstance(limit.get("total"), int):
+ return max(0, int(limit["total"]))
+ if isinstance(limit, int):
+ return max(0, int(limit))
+ return default
+
+
+def _project(row: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]:
+ include = body.get("include_attributes")
+ exclude = set(body.get("exclude_attributes") or [])
+ if include is True or include is None:
+ projected = deepcopy(row)
+ elif isinstance(include, list):
+ projected = {"id": row.get("id")}
+ for key in include:
+ if key in row:
+ projected[str(key)] = deepcopy(row[key])
+ else:
+ projected = deepcopy(row)
+ for key in exclude:
+ projected.pop(str(key), None)
+ return projected
+
+
+def _matches(row: dict[str, Any], filter_expr: Any) -> bool:
+ if filter_expr is None:
+ return True
+ if not isinstance(filter_expr, list) or not filter_expr:
+ return True
+ head = filter_expr[0]
+ if head == "And":
+ clauses = filter_expr[1] if len(filter_expr) > 1 else []
+ return all(_matches(row, clause) for clause in clauses)
+ if head == "Or":
+ clauses = filter_expr[1] if len(filter_expr) > 1 else []
+ return any(_matches(row, clause) for clause in clauses)
+ if len(filter_expr) < 3:
+ return True
+ field, operator, expected = filter_expr[0], str(filter_expr[1]), filter_expr[2]
+ actual = row.get(str(field))
+ if operator == "Eq":
+ return actual == expected
+ if operator == "NotEq":
+ return actual != expected
+ if operator == "In":
+ return actual in set(expected if isinstance(expected, list) else [expected])
+ if operator == "Gt":
+ return actual is not None and actual > expected
+ if operator == "Gte":
+ return actual is not None and actual >= expected
+ if operator == "Lt":
+ return actual is not None and actual < expected
+ if operator == "Lte":
+ return actual is not None and actual <= expected
+ return True
diff --git a/e2e/stubs/fake_turbopuffer/requirements.txt b/e2e/stubs/fake_turbopuffer/requirements.txt
new file mode 100644
index 00000000..2081c2ae
--- /dev/null
+++ b/e2e/stubs/fake_turbopuffer/requirements.txt
@@ -0,0 +1,2 @@
+fastapi>=0.136.1
+uvicorn[standard]>=0.47.0
diff --git a/e2e/tests/_helpers.py b/e2e/tests/_helpers.py
index 84d8df0f..24f5a202 100644
--- a/e2e/tests/_helpers.py
+++ b/e2e/tests/_helpers.py
@@ -25,11 +25,7 @@
def unique_name(prefix: str = "e2e") -> str:
- """Return a unique, Qdrant-safe collection name.
-
- Qdrant collection names are limited; we keep the prefix tiny and use
- 8 hex characters of a UUID4 to stay well under any provider limit.
- """
+ """Return a compact unique collection name."""
return f"{prefix}_{uuid.uuid4().hex[:8]}"
diff --git a/e2e/tests/api/test_collections.py b/e2e/tests/api/test_collections.py
index 1a2db2f9..fd381d8a 100644
--- a/e2e/tests/api/test_collections.py
+++ b/e2e/tests/api/test_collections.py
@@ -43,7 +43,6 @@ async def test_create_collection_returns_201_with_defaults(
assert coll["dimension"] == 1536
assert coll["embedding_model"] == "text-embedding-3-small"
assert coll["embedding_provider"] == "openai_compatible"
- assert coll["vector_store_provider"] == "qdrant"
assert coll["chunk_size"] == 512
assert coll["chunk_overlap"] == 50
assert coll["chunk_strategy"] == "paragraph"
diff --git a/e2e/tests/api/test_errors.py b/e2e/tests/api/test_errors.py
index 43bc6927..037fce4b 100644
--- a/e2e/tests/api/test_errors.py
+++ b/e2e/tests/api/test_errors.py
@@ -128,7 +128,6 @@ async def test_409_duplicate_collection_name(
json={
"name": first["name"],
"description": "dup attempt",
- "vector_store_provider": "qdrant",
"chunk_size": 512,
"chunk_overlap": 50,
"chunk_strategy": "paragraph",
@@ -195,9 +194,8 @@ async def test_422_bad_enum_on_create_collection(
"/v1/collections",
json={
"name": unique_name("enum"),
- "vector_store_provider": "not-a-real-provider",
"chunk_strategy": "paragraph",
- "default_search_mode": "semantic",
+ "default_search_mode": "not-a-real-mode",
},
)
assert resp.status_code == 422, resp.text
diff --git a/e2e/tests/api/test_health.py b/e2e/tests/api/test_health.py
index 0be70d3f..a573d1f4 100644
--- a/e2e/tests/api/test_health.py
+++ b/e2e/tests/api/test_health.py
@@ -32,7 +32,9 @@ async def test_health_readiness_reports_each_dependency(
assert dep in body, f"missing dependency key {dep!r} in {body!r}"
assert isinstance(body[dep], bool)
assert "vector_store_provider" in body
- assert "qdrant" in body
+ assert body["vector_store_provider"] == "turbopuffer"
+ assert "turbopuffer" in body
+ assert "qdrant" not in body
async def test_health_readiness_status_matches_http_code(
diff --git a/e2e/tests/api/test_realtime_sse.py b/e2e/tests/api/test_realtime_sse.py
index c09e6934..4b92be4f 100644
--- a/e2e/tests/api/test_realtime_sse.py
+++ b/e2e/tests/api/test_realtime_sse.py
@@ -9,7 +9,6 @@
- GET /v1/admin/realtime/{provider_slug}/sources
- GET /v1/admin/realtime/{provider_slug}/sync-jobs
- GET /v1/admin/realtime/backups
-- GET /v1/admin/realtime/vector-migrations
- GET /v1/admin/realtime/access/overview
- GET /v1/admin/realtime/access/logs
- GET /v1/admin/realtime/audit
@@ -176,20 +175,6 @@ async def test_realtime_backups_stream(
await _assert_api_key_blocked(api_key_client, path)
-async def test_realtime_vector_migrations_stream(
- admin_client: httpx.AsyncClient,
- unauth_client: httpx.AsyncClient,
- api_key_client: Callable[..., Awaitable[httpx.AsyncClient]],
-) -> None:
- path = "/v1/admin/realtime/vector-migrations"
- snapshot = await _first_snapshot(admin_client, path)
- assert snapshot["topic"].startswith("vector-migrations:")
- assert "payload" in snapshot
-
- await _assert_unauth_blocked(unauth_client, path)
- await _assert_api_key_blocked(api_key_client, path)
-
-
async def test_realtime_usage_stream(
admin_client: httpx.AsyncClient,
unauth_client: httpx.AsyncClient,
diff --git a/e2e/tests/api/test_settings.py b/e2e/tests/api/test_settings.py
index 785060e3..6df7f6df 100644
--- a/e2e/tests/api/test_settings.py
+++ b/e2e/tests/api/test_settings.py
@@ -75,9 +75,17 @@ async def test_settings_get_returns_specs_and_values(
assert "values" in body and isinstance(body["values"], dict)
spec_keys = {spec["key"] for spec in body["specs"]}
- for sample in ("chat_temperature", "embedding_api_key", "max_upload_size_mb"):
+ for sample in (
+ "chat_temperature",
+ "embedding_api_key",
+ "max_upload_size_mb",
+ "turbopuffer_api_key",
+ "turbopuffer_base_url",
+ ):
assert sample in spec_keys, f"expected setting key {sample!r} in spec list"
assert sample in body["values"], f"missing value entry for {sample!r}"
+ assert "qdrant_url" not in spec_keys
+ assert "qdrant_required" not in spec_keys
for spec in body["specs"]:
for key in ("key", "group", "label", "description", "kind", "default", "options", "secret"):
@@ -246,6 +254,29 @@ async def test_settings_test_validates_chat_provider_settings(
assert set(body["checked"]) >= {"chat_base_url", "chat_model", "chat_temperature"}
+async def test_settings_test_validates_turbopuffer_settings(
+ admin_client: httpx.AsyncClient,
+ fake_turbopuffer_base: str,
+) -> None:
+ resp = await admin_client.post(
+ "/v1/admin/settings/test",
+ json={
+ "values": {
+ "turbopuffer_api_key": "e2e-fake-turbopuffer-key",
+ "turbopuffer_base_url": fake_turbopuffer_base,
+ "turbopuffer_region": "e2e",
+ },
+ },
+ )
+ body = assert_envelope(resp, 200)
+ assert body["status"] == "ok"
+ assert set(body["checked"]) >= {
+ "turbopuffer_api_key",
+ "turbopuffer_base_url",
+ "turbopuffer_region",
+ }
+
+
async def test_settings_test_rejects_broken_base_url(
admin_client: httpx.AsyncClient,
) -> None:
diff --git a/e2e/tests/api/test_vector_storage.py b/e2e/tests/api/test_vector_storage.py
index a6aa5962..de5a607b 100644
--- a/e2e/tests/api/test_vector_storage.py
+++ b/e2e/tests/api/test_vector_storage.py
@@ -10,8 +10,6 @@
from typing import Any
import httpx
-import pytest
-
from tests._helpers import assert_envelope
@@ -36,25 +34,14 @@ async def test_vector_storage_overview_returns_provider_shape(
resp = await admin_client.get("/v1/admin/vector-storage/overview")
body = assert_envelope(resp, 200)
- for key in (
- "fallback_provider",
- "configured_providers",
- "provider_health",
- "collections",
- "totals",
- ):
+ for key in ("provider", "health", "collections", "totals"):
assert key in body, f"missing top-level key {key!r} in {body!r}"
- assert isinstance(body["fallback_provider"], str) and body["fallback_provider"]
- assert isinstance(body["configured_providers"], list)
- # bigRAG can return a fallback_provider label distinct from the listed
- # configured providers (e.g., "collection" when collection-level provider
- # overrides are in effect). Just assert the field is non-empty here.
- assert isinstance(body["provider_health"], dict)
- for provider in body["configured_providers"]:
- assert provider in body["provider_health"], (
- f"provider_health missing entry for {provider!r}"
- )
+ assert body["provider"] == "turbopuffer"
+ assert isinstance(body["health"], dict)
+ assert body["health"].get("status") in {"ok", "error"}
+ for removed_key in ("fallback_provider", "configured_providers", "provider_health"):
+ assert removed_key not in body
assert isinstance(body["collections"], list)
totals = body["totals"]
@@ -84,74 +71,8 @@ async def test_vector_storage_overview_reflects_collection_changes(
assert after_body["totals"]["collections"] >= before_count + 1
entry = next(c for c in after_body["collections"] if c["name"] == new_coll["name"])
- for key in ("name", "provider", "documents", "chunks", "bytes"):
+ for key in ("name", "documents", "chunks", "bytes"):
assert key in entry
+ assert "provider" not in entry
assert entry["documents"] == 0
assert entry["chunks"] == 0
-
-
-async def test_vector_migrations_requires_admin_session(
- unauth_client: httpx.AsyncClient,
-) -> None:
- resp = await unauth_client.get("/v1/admin/vector-storage/migrations")
- assert resp.status_code == 401, resp.text
-
-
-async def test_vector_migrations_rejects_api_key(
- api_key_client: Callable[..., Awaitable[httpx.AsyncClient]],
-) -> None:
- client = await api_key_client()
- resp = await client.get("/v1/admin/vector-storage/migrations")
- assert resp.status_code in (401, 403), resp.text
-
-
-async def test_vector_migrations_list_returns_shape(
- admin_client: httpx.AsyncClient,
-) -> None:
- resp = await admin_client.get(
- "/v1/admin/vector-storage/migrations",
- params={"include_total": "true", "limit": 1},
- )
- body = assert_envelope(resp, 200)
- assert "jobs" in body
- assert "total" in body
- assert "next_cursor" in body
- assert isinstance(body["jobs"], list)
- assert isinstance(body["total"], int)
-
-
-async def test_vector_migration_unknown_collection_returns_400(
- admin_client: httpx.AsyncClient,
-) -> None:
- resp = await admin_client.post(
- "/v1/admin/vector-storage/migrations",
- json={"collection": "missing_collection", "target_provider": "turbopuffer"},
- )
- assert resp.status_code in (400, 409), resp.text
-
-
-async def test_vector_migration_same_provider_returns_409(
- admin_client: httpx.AsyncClient,
- collection: Callable[..., Awaitable[dict[str, Any]]],
-) -> None:
- coll = await collection()
- resp = await admin_client.post(
- "/v1/admin/vector-storage/migrations",
- json={"collection": coll["name"], "target_provider": coll["vector_store_provider"]},
- )
- assert resp.status_code == 409, resp.text
-
-
-async def test_vector_migration_unconfigured_target_returns_400(
- admin_client: httpx.AsyncClient,
- collection: Callable[..., Awaitable[dict[str, Any]]],
-) -> None:
- overview = assert_envelope(await admin_client.get("/v1/admin/vector-storage/overview"), 200)
- if "turbopuffer" in overview["configured_providers"]:
- pytest.skip("turbopuffer is configured in this e2e environment")
- coll = await collection()
- resp = await admin_client.post(
- "/v1/admin/vector-storage/migrations",
- json={"collection": coll["name"], "target_provider": "turbopuffer"},
- )
- assert resp.status_code == 400, resp.text
diff --git a/e2e/tests/sdk_python/test_collections.py b/e2e/tests/sdk_python/test_collections.py
index 45a7971a..bfdf6033 100644
--- a/e2e/tests/sdk_python/test_collections.py
+++ b/e2e/tests/sdk_python/test_collections.py
@@ -32,7 +32,6 @@ async def test_collections_create_returns_full_collection(
{
"name": name,
"description": "sdk create",
- "vector_store_provider": "qdrant",
"dimension": 1536,
"chunk_size": 512,
"chunk_overlap": 50,
@@ -48,7 +47,6 @@ async def test_collections_create_returns_full_collection(
"description",
"embedding_provider",
"embedding_model",
- "vector_store_provider",
"dimension",
"chunk_size",
"chunk_overlap",
diff --git a/e2e/tests/sdk_typescript/auth.test.ts b/e2e/tests/sdk_typescript/auth.test.ts
index c04f9b6e..a1d5673a 100644
--- a/e2e/tests/sdk_typescript/auth.test.ts
+++ b/e2e/tests/sdk_typescript/auth.test.ts
@@ -64,7 +64,6 @@ describe("AuthResource", () => {
const body: Parameters[0] = {
name: collName,
description: "auth pin test",
- vector_store_provider: "qdrant",
dimension: 1536,
chunk_size: 512,
chunk_overlap: 50,
diff --git a/e2e/tests/sdk_typescript/collections.test.ts b/e2e/tests/sdk_typescript/collections.test.ts
index 1e4301c1..3d4a7e38 100644
--- a/e2e/tests/sdk_typescript/collections.test.ts
+++ b/e2e/tests/sdk_typescript/collections.test.ts
@@ -19,7 +19,6 @@ async function createTempCollection(): Promise {
const body: Parameters[0] = {
name: uniqueName("sdkcoll"),
description: "sdk collections.test",
- vector_store_provider: "qdrant",
dimension: 1536,
chunk_size: 512,
chunk_overlap: 50,
@@ -38,7 +37,6 @@ describe("CollectionsResource", () => {
expect(typeof created.id).toBe("string");
expect(created.id.length).toBeGreaterThan(0);
expect(typeof created.name).toBe("string");
- expect(created.vector_store_provider).toBe("qdrant");
expect(typeof created.dimension).toBe("number");
expect(created.dimension).toBeGreaterThan(0);
expect(created.document_count).toBe(0);
diff --git a/e2e/tests/sdk_typescript/helpers.ts b/e2e/tests/sdk_typescript/helpers.ts
index f7f01b63..9d88ab42 100644
--- a/e2e/tests/sdk_typescript/helpers.ts
+++ b/e2e/tests/sdk_typescript/helpers.ts
@@ -256,9 +256,6 @@ export async function createCollection(
const body: CreateCollectionBody = {
name: uniqueName("e2e"),
description: "sdk e2e collection",
- vector_store_provider: "qdrant",
- // bigRAG requires `dimension` for openai_compatible providers (the
- // default in e2e). text-embedding-3-small uses 1536.
dimension: 1536,
chunk_size: 512,
chunk_overlap: 50,
From a70f685114c65fbb7c266e5b21c592c7ec6058bc Mon Sep 17 00:00:00 2001
From: Bigint <69431456+bigint@users.noreply.github.com>
Date: Thu, 21 May 2026 23:03:11 +0530
Subject: [PATCH 3/9] style: reformat e2e collection and realtime tests
---
e2e/tests/api/test_collections.py | 25 ++++++------------------
e2e/tests/api/test_errors.py | 16 ++++-----------
e2e/tests/api/test_realtime_sse.py | 16 ++++++---------
e2e/tests/api/test_vector_storage.py | 1 +
e2e/tests/sdk_python/test_collections.py | 2 +-
5 files changed, 18 insertions(+), 42 deletions(-)
diff --git a/e2e/tests/api/test_collections.py b/e2e/tests/api/test_collections.py
index fd381d8a..a2a77aa5 100644
--- a/e2e/tests/api/test_collections.py
+++ b/e2e/tests/api/test_collections.py
@@ -29,7 +29,6 @@
from tests._helpers import assert_envelope, unique_name
-
# ---------------------------------------------------------------------------
# Create
# ---------------------------------------------------------------------------
@@ -245,9 +244,7 @@ async def test_delete_collection_also_removes_documents(
resp = await admin_client.delete(f"/v1/collections/{coll['name']}")
assert resp.status_code == 200, resp.text
- follow_up = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/{doc['id']}"
- )
+ follow_up = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}")
assert follow_up.status_code == 404, follow_up.text
@@ -328,9 +325,7 @@ async def test_reembed_kicks_off_for_existing_collection(
async def test_reembed_unknown_collection_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.post(
- f"/v1/collections/{unique_name('missing')}/reembed"
- )
+ resp = await admin_client.post(f"/v1/collections/{unique_name('missing')}/reembed")
assert resp.status_code == 404, resp.text
@@ -354,9 +349,7 @@ async def test_truncate_removes_documents_but_keeps_collection(
async def test_truncate_unknown_collection_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.post(
- f"/v1/collections/{unique_name('missing')}/truncate"
- )
+ resp = await admin_client.post(f"/v1/collections/{unique_name('missing')}/truncate")
assert resp.status_code == 404, resp.text
@@ -370,9 +363,7 @@ async def test_event_token_returns_short_lived_token(
collection: Callable[..., Awaitable[dict[str, Any]]],
) -> None:
coll = await collection()
- resp = await admin_client.post(
- f"/v1/collections/{coll['name']}/events/token"
- )
+ resp = await admin_client.post(f"/v1/collections/{coll['name']}/events/token")
body = assert_envelope(resp, 200)
assert isinstance(body["token"], str) and len(body["token"]) > 0
assert isinstance(body["expires_in"], int) and body["expires_in"] > 0
@@ -381,9 +372,7 @@ async def test_event_token_returns_short_lived_token(
async def test_event_token_unknown_collection_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.post(
- f"/v1/collections/{unique_name('missing')}/events/token"
- )
+ resp = await admin_client.post(f"/v1/collections/{unique_name('missing')}/events/token")
assert resp.status_code == 404, resp.text
@@ -393,9 +382,7 @@ async def test_collection_events_unknown_collection_returns_404(
"""Hit the SSE GET endpoint just enough to verify it 404s for unknown
collections without consuming the stream (full SSE flow is covered by
test_sse_events.py)."""
- resp = await admin_client.get(
- f"/v1/collections/{unique_name('missing')}/events"
- )
+ resp = await admin_client.get(f"/v1/collections/{unique_name('missing')}/events")
assert resp.status_code == 404, resp.text
diff --git a/e2e/tests/api/test_errors.py b/e2e/tests/api/test_errors.py
index 037fce4b..59b98632 100644
--- a/e2e/tests/api/test_errors.py
+++ b/e2e/tests/api/test_errors.py
@@ -26,7 +26,6 @@
from tests._helpers import unique_name
-
# ---------------------------------------------------------------------------
# 401 — missing authentication on a protected endpoint.
# ---------------------------------------------------------------------------
@@ -93,23 +92,18 @@ async def test_404_unknown_document(
) -> None:
coll = await collection()
resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/"
- "00000000-0000-0000-0000-000000000000"
+ f"/v1/collections/{coll['name']}/documents/00000000-0000-0000-0000-000000000000"
)
assert resp.status_code == 404, resp.text
async def test_404_unknown_user(admin_client: httpx.AsyncClient) -> None:
- resp = await admin_client.delete(
- "/v1/admin/users/00000000-0000-0000-0000-000000000000"
- )
+ resp = await admin_client.delete("/v1/admin/users/00000000-0000-0000-0000-000000000000")
assert resp.status_code == 404, resp.text
async def test_404_unknown_webhook(admin_client: httpx.AsyncClient) -> None:
- resp = await admin_client.get(
- "/v1/admin/webhooks/00000000-0000-0000-0000-000000000000"
- )
+ resp = await admin_client.get("/v1/admin/webhooks/00000000-0000-0000-0000-000000000000")
assert resp.status_code == 404, resp.text
@@ -222,9 +216,7 @@ async def test_422_wrong_type_on_create_collection(
async def test_422_out_of_range_pagination(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.get(
- "/v1/admin/api-keys", params={"limit": 0}
- )
+ resp = await admin_client.get("/v1/admin/api-keys", params={"limit": 0})
assert resp.status_code == 422, resp.text
diff --git a/e2e/tests/api/test_realtime_sse.py b/e2e/tests/api/test_realtime_sse.py
index 4b92be4f..9314bf51 100644
--- a/e2e/tests/api/test_realtime_sse.py
+++ b/e2e/tests/api/test_realtime_sse.py
@@ -32,7 +32,6 @@
import pytest
from conftest import sse_events
-from tests._helpers import unique_name
SSE_TIMEOUT_SECONDS = 10.0
@@ -51,9 +50,7 @@ async def _read() -> dict[str, Any]:
if event.event == "snapshot":
return json.loads(event.data)
if event.event == "error":
- raise AssertionError(
- f"SSE error event for {path!r}: {event.data}"
- )
+ raise AssertionError(f"SSE error event for {path!r}: {event.data}")
raise AssertionError(f"no snapshot event received from {path!r}")
return await asyncio.wait_for(_read(), timeout=timeout)
@@ -64,7 +61,9 @@ async def _assert_unauth_blocked(
path: str,
) -> None:
resp = await unauth_client.get(path)
- assert resp.status_code == 401, f"{path}: expected 401 unauth, got {resp.status_code} {resp.text}"
+ assert resp.status_code == 401, (
+ f"{path}: expected 401 unauth, got {resp.status_code} {resp.text}"
+ )
async def _assert_api_key_blocked(
@@ -149,9 +148,7 @@ async def test_realtime_collection_documents_batch_status_requires_ids(
collection: Callable[..., Awaitable[dict[str, Any]]],
) -> None:
coll = await collection()
- path = (
- f"/v1/admin/realtime/collections/{coll['name']}/documents/batch-status"
- )
+ path = f"/v1/admin/realtime/collections/{coll['name']}/documents/batch-status"
resp = await admin_client.get(path)
assert resp.status_code == 400, resp.text
@@ -288,8 +285,7 @@ async def test_realtime_upload_session_unauth(
unauth_client: httpx.AsyncClient,
) -> None:
path = (
- "/v1/admin/realtime/collections/nope/upload-sessions/"
- "00000000-0000-0000-0000-000000000000"
+ "/v1/admin/realtime/collections/nope/upload-sessions/00000000-0000-0000-0000-000000000000"
)
resp = await unauth_client.get(path)
assert resp.status_code == 401, resp.text
diff --git a/e2e/tests/api/test_vector_storage.py b/e2e/tests/api/test_vector_storage.py
index de5a607b..d14b7a08 100644
--- a/e2e/tests/api/test_vector_storage.py
+++ b/e2e/tests/api/test_vector_storage.py
@@ -10,6 +10,7 @@
from typing import Any
import httpx
+
from tests._helpers import assert_envelope
diff --git a/e2e/tests/sdk_python/test_collections.py b/e2e/tests/sdk_python/test_collections.py
index bfdf6033..7711514c 100644
--- a/e2e/tests/sdk_python/test_collections.py
+++ b/e2e/tests/sdk_python/test_collections.py
@@ -18,8 +18,8 @@
from typing import Any
import pytest
-
from bigrag import BigRAG, NotFoundError
+
from tests._helpers import unique_name
From 395366e1344c003e68a772346b296c9895318d39 Mon Sep 17 00:00:00 2001
From: Bigint <69431456+bigint@users.noreply.github.com>
Date: Thu, 21 May 2026 23:08:24 +0530
Subject: [PATCH 4/9] Remove Qdrant provider cleanup
---
AGENTS.md | 11 ++-
CONTRIBUTING.md | 6 +-
README.md | 19 +++--
api/bigrag/routers/admin_vector_storage.py | 12 ++--
api/bigrag/services/collection_provision.py | 12 ++--
api/bigrag/services/retrieval/modes.py | 13 +---
api/bigrag/services/retrieval/orchestrate.py | 14 ----
api/bigrag/services/vector_store/__init__.py | 8 +--
api/bigrag/services/vector_store/_util.py | 36 ----------
api/bigrag/services/vector_store/base.py | 11 +--
api/bigrag/services/vector_store/facade.py | 22 +++---
.../services/vector_store/turbopuffer.py | 4 --
bigrag.toml | 7 +-
e2e/tests/api/test_health.py | 4 --
e2e/tests/api/test_settings.py | 4 +-
e2e/tests/api/test_vector_storage.py | 5 +-
website/components/home/code-section.tsx | 2 +-
.../docs/api-reference/admin-realtime.mdx | 3 +-
.../content/docs/api-reference/backups.mdx | 6 +-
.../docs/api-reference/collections.mdx | 14 ++--
website/content/docs/api-reference/health.mdx | 6 +-
.../docs/api-reference/instance-settings.mdx | 12 ++--
.../content/docs/api-reference/vectors.mdx | 70 ++-----------------
website/content/docs/comparison.mdx | 4 +-
.../content/docs/concepts/architecture.mdx | 59 +++++++---------
website/content/docs/concepts/collections.mdx | 11 +--
website/content/docs/concepts/search.mdx | 6 +-
.../docs/cookbook/multi-tenant-saas.mdx | 8 +--
website/content/docs/deployment/docker.mdx | 32 +++------
.../content/docs/deployment/encryption.mdx | 12 ++--
.../content/docs/deployment/production.mdx | 38 ++++------
website/content/docs/deployment/railway.mdx | 39 ++++-------
.../docs/getting-started/configuration.mdx | 20 +++---
.../docs/getting-started/installation.mdx | 6 +-
website/content/docs/index.mdx | 12 ++--
.../content/docs/migration/from-pinecone.mdx | 10 +--
website/content/docs/sdks/python.mdx | 24 ++-----
website/content/docs/sdks/typescript.mdx | 13 ----
38 files changed, 185 insertions(+), 410 deletions(-)
delete mode 100644 api/bigrag/services/vector_store/_util.py
diff --git a/AGENTS.md b/AGENTS.md
index 3ac68271..16f1e4b0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,12 +2,12 @@
## Project Structure
-- `api/` — Python/FastAPI backend (Docling ingestion + Qdrant/Turbopuffer vector DB)
+- `api/` — Python/FastAPI backend (Docling ingestion + Turbopuffer vector search)
- `sdks/typescript/` — TypeScript SDK (`@bigrag/client`)
- `sdks/python/` — Python SDK (`bigrag`)
- `app/` — admin UI (Vite + TanStack Router + Tailwind v4 + Base UI, `@bigrag/app`)
- `website/` — Documentation site (Next.js + Fumadocs, content in `website/content/docs/`)
-- `e2e/` — pytest + vitest end-to-end suites against a fake-OpenAI + real Postgres/Redis/Qdrant stack
+- `e2e/` — pytest + vitest end-to-end suites against fake OpenAI/Turbopuffer services plus real Postgres/Redis
## Style Guide
@@ -33,8 +33,8 @@ When adding new code, prefer the smallest meaningful module instead of dropping
## Tech Stack
-- **Backend**: Python 3.12+, FastAPI, SQLAlchemy 2 (async) + asyncpg, Alembic, qdrant-client, docling, openai, cohere, cryptography (Fernet for at-rest encryption of provider secrets), dramatiq (Redis broker)
-- **Vector DB**: Qdrant default; turbopuffer alternative (selected per collection)
+- **Backend**: Python 3.12+, FastAPI, SQLAlchemy 2 (async) + asyncpg, Alembic, docling, openai, cohere, cryptography (Fernet for at-rest encryption of provider secrets), dramatiq (Redis broker)
+- **Vector DB**: Turbopuffer
- **Metadata DB**: PostgreSQL 17
- **Ingestion**: Docling (PDF, DOCX, PPTX, XLSX, HTML, Markdown, images)
- **Embedding**: OpenAI, Cohere, Voyage, and OpenAI-compatible providers
@@ -124,7 +124,7 @@ If a feature is removed, remove it from the docs too. Never leave stale referenc
```bash
./dev.sh # starts infra + backend + worker
-./dev.sh --infra # postgres + redis + qdrant only
+./dev.sh --infra # postgres + redis only
./dev.sh --website # docs site only
pnpm dev:app # admin UI on localhost:3000
```
@@ -133,7 +133,6 @@ pnpm dev:app # admin UI on localhost:3000
- Backend API: http://localhost:4000 (Swagger docs at /docs)
- Postgres: localhost:5432
- Redis: localhost:6379
-- Qdrant: localhost:6333
`dev.sh` only tears down the docker stack that it started — if you ran `docker compose up` separately before invoking `dev.sh`, it leaves that running on exit.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 980cfb0b..cbdda1be 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,7 +8,7 @@ Thank you for your interest in contributing to bigRAG. This guide will help you
- **Python 3.12+** with [uv](https://docs.astral.sh/uv/)
- **Node.js 20+** with [pnpm](https://pnpm.io/) (via corepack)
-- **Docker** and **Docker Compose** — for Postgres, Redis, Qdrant
+- **Docker** and **Docker Compose** — for Postgres and Redis
### Development Setup
@@ -31,7 +31,7 @@ Or manually:
```bash
# Start infrastructure
-docker compose up postgres redis qdrant -d
+docker compose up postgres redis -d
# Set up the Python backend
cd api
@@ -60,7 +60,7 @@ bigrag/
├── sdks/python/ # Python SDK (bigrag)
├── app/ # Admin UI (TanStack Router + React)
├── website/ # Docs site (Next.js + Fumadocs)
-├── docker-compose.yml # Full stack (Postgres, Redis, Qdrant, API)
+├── docker-compose.yml # Full stack (Postgres, Redis, API)
├── biome.jsonc # Biome linting config for TypeScript
├── pnpm-workspace.yaml # pnpm workspace config
├── dev.sh # One-command dev setup
diff --git a/README.md b/README.md
index df85e5a9..bf07a538 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Open-source, self-hostable RAG platform. Upload documents, auto-chunk, embed, an
- **Document ingestion** — PDF, DOCX, PPTX, HTML, Markdown, images, and more via [Docling](https://github.com/DS4SD/docling)
- **Embedding providers** — OpenAI, OpenAI-compatible gateways, Cohere, and Voyage
- **Embedding presets** — save named provider/model configs once, reuse across collections
-- **Vector search** — semantic, keyword, and hybrid search modes via [Qdrant](https://qdrant.io) or [turbopuffer](https://turbopuffer.com)
+- **Vector search** — semantic, keyword, and hybrid search modes via [Turbopuffer](https://turbopuffer.com)
- **Reranking** — Cohere reranking for improved result relevance
- **Multi-collection queries** — search across collections in a single request
- **Generated chat** — stateless backend-grounded playground chat with streaming and citations
@@ -31,7 +31,7 @@ Open-source, self-hostable RAG platform. Upload documents, auto-chunk, embed, an
docker compose up -d
```
-This starts bigRAG API, worker, admin UI, Postgres, Redis, and Qdrant (or turbopuffer per collection). Open http://localhost:3000 for the admin UI or http://localhost:4000/docs for the interactive API docs.
+This starts bigRAG API, worker, admin UI, Postgres, and Redis. Configure Turbopuffer before ingesting or querying collections. Open http://localhost:3000 for the admin UI or http://localhost:4000/docs for the interactive API docs.
```bash
# Create a collection
@@ -52,7 +52,7 @@ curl -X POST http://localhost:4000/v1/collections/docs/query \
### Development
```bash
-./dev.sh # starts Postgres, Redis, Qdrant, the API with hot reload, and the worker
+./dev.sh # starts Postgres, Redis, the API with hot reload, and the worker
```
### Docker Images
@@ -90,7 +90,7 @@ graph TD
Worker -->|parse| Docling[Docling
PDF, DOCX, HTML, Images]
Worker -->|embed| Embedding[Embedding provider
OpenAI / compatible / Cohere / Voyage]
- Worker -->|store vectors| Vectors[(Qdrant
or turbopuffer)]
+ Worker -->|store vectors| Vectors[(Turbopuffer)]
Query -->|search| Vectors
Query -->|embed query| Embedding
@@ -303,13 +303,10 @@ Backend logging defaults to `debug` / `text` for local development. Use `BIGRAG_
| `BIGRAG_DB_POOL_MIN` | Min Postgres pool size | `5` |
| `BIGRAG_DB_POOL_MAX` | Max Postgres pool size | `50` |
| `BIGRAG_MIGRATION_TIMEOUT_SECONDS` | Startup migration check timeout (`0` disables the timeout) | `60` |
-| `BIGRAG_QDRANT_URL` | Qdrant URL | `http://localhost:6333` |
-| `BIGRAG_QDRANT_CONNECT_TIMEOUT_SECONDS` | Qdrant startup connection timeout (`0` disables the timeout) | `10` |
-| `BIGRAG_QDRANT_REQUIRED` | Fail API startup if Qdrant cannot be reached | `false` |
-| `BIGRAG_QDRANT_SEARCH_EF` | Optional Qdrant HNSW search recall/latency tuning | — |
-| `BIGRAG_TURBOPUFFER_API_KEY` | turbopuffer API key (only required for collections using turbopuffer) | — |
-| `BIGRAG_TURBOPUFFER_REGION` | turbopuffer region | `aws-us-east-1` |
-| `BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX` | Prefix prepended to turbopuffer namespace names | `bigrag_` |
+| `BIGRAG_TURBOPUFFER_API_KEY` | Turbopuffer API key | — |
+| `BIGRAG_TURBOPUFFER_REGION` | Turbopuffer region | `aws-us-east-1` |
+| `BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX` | Prefix prepended to Turbopuffer namespace names | `bigrag_` |
+| `BIGRAG_TURBOPUFFER_BASE_URL` | Optional Turbopuffer-compatible endpoint for local/e2e services | — |
| `BIGRAG_REDIS_URL` | Redis URL | `redis://localhost:6379/0` |
| `BIGRAG_ENV` | `dev` or `prod` (prod enables startup safety checks) | `dev` |
| `BIGRAG_TRUSTED_PROXIES` | JSON array of trusted proxy CIDRs used to honor `X-Forwarded-For` for audit and access logs | `[]` |
diff --git a/api/bigrag/routers/admin_vector_storage.py b/api/bigrag/routers/admin_vector_storage.py
index 70d70d12..a4e4114f 100644
--- a/api/bigrag/routers/admin_vector_storage.py
+++ b/api/bigrag/routers/admin_vector_storage.py
@@ -7,6 +7,7 @@
from bigrag.db.models import Collection, Document
from bigrag.db.session import get_session
from bigrag.middleware.auth import require_admin_session
+from bigrag.services.health import categorize_dependency_error
from bigrag.services.vector_store import vector_store
router = APIRouter(prefix="/v1/admin/vector-storage", tags=["admin:vector-storage"])
@@ -17,7 +18,11 @@ async def vector_storage_overview(
_: dict = Depends(require_admin_session),
session: AsyncSession = Depends(get_session),
) -> dict[str, object]:
- provider_health = await vector_store.provider_health()
+ health: dict[str, object] = {"status": "ok", "error": None}
+ try:
+ await vector_store.health_check()
+ except Exception as exc:
+ health = {"status": "error", "error": categorize_dependency_error(exc)}
rows = (
await session.execute(
sa.select(
@@ -47,9 +52,8 @@ async def vector_storage_overview(
"bytes": sum(item["bytes"] for item in collections),
}
return {
- "fallback_provider": vector_store.provider,
- "configured_providers": list(vector_store.configured_providers),
- "provider_health": provider_health,
+ "provider": "turbopuffer",
+ "health": health,
"collections": collections,
"totals": totals,
}
diff --git a/api/bigrag/services/collection_provision.py b/api/bigrag/services/collection_provision.py
index 8afd4683..c34f6f9e 100644
--- a/api/bigrag/services/collection_provision.py
+++ b/api/bigrag/services/collection_provision.py
@@ -38,13 +38,13 @@ async def verify_embedding_credentials(
def vector_store_unavailable_detail() -> str:
return (
- "Turbopuffer is not configured. Save a turbopuffer API key in Vector Storage "
+ "Turbopuffer is not configured. Save a turbopuffer API key in Settings "
"before creating a collection."
)
def ensure_vector_store_available() -> None:
- if vector_store.provider in vector_store.configured_providers:
+ if vector_store.is_configured():
return
raise HTTPException(
status_code=400,
@@ -68,8 +68,8 @@ async def create_vector_store_collection(
if "API key is not configured" in message or "client is not connected" in message:
raise HTTPException(
status_code=400,
- detail=vector_store_unavailable_detail(),
- ) from e
+ detail=vector_store_unavailable_detail(),
+ ) from e
logger.warning(
"vector collection create failed",
collection=body.name,
@@ -78,7 +78,7 @@ async def create_vector_store_collection(
)
raise HTTPException(
status_code=502,
- detail="Unable to create vector collection. Check Vector Storage settings.",
+ detail="Unable to create vector collection. Check Turbopuffer settings.",
) from e
except httpx.HTTPError as e:
logger.warning(
@@ -89,5 +89,5 @@ async def create_vector_store_collection(
)
raise HTTPException(
status_code=502,
- detail="Unable to create vector collection. Check Vector Storage settings.",
+ detail="Unable to create vector collection. Check Turbopuffer settings.",
) from e
diff --git a/api/bigrag/services/retrieval/modes.py b/api/bigrag/services/retrieval/modes.py
index a0e5d3c1..8104db3f 100644
--- a/api/bigrag/services/retrieval/modes.py
+++ b/api/bigrag/services/retrieval/modes.py
@@ -8,7 +8,7 @@
from bigrag.services.embedding import EmbeddingModel
from bigrag.services.retrieval.cache import embed_query_with_cache
from bigrag.services.retrieval.fusion import keyword_patterns, keyword_score, reciprocal_rank_fusion
-from bigrag.services.vector_store import VectorStoreFeatureError, VectorStoreProvider, vector_store
+from bigrag.services.vector_store import vector_store
async def keyword_search(
@@ -17,7 +17,6 @@ async def keyword_search(
query_terms: list[str],
top_k: int,
filter_expr: FilterExpression | None,
- vector_store_provider: VectorStoreProvider | None,
timings: dict[str, float],
) -> list[dict]:
t0 = time.monotonic()
@@ -27,9 +26,8 @@ async def keyword_search(
query_terms=query_terms,
top_k=top_k,
filters=filter_expr,
- provider=vector_store_provider,
)
- except VectorStoreFeatureError as exc:
+ except RuntimeError as exc:
raise ValidationError(str(exc)) from exc
timings["search_ms"] = (time.monotonic() - t0) * 1000
@@ -53,7 +51,6 @@ async def hybrid_search(
top_k: int,
max_top_k: int,
filter_expr: FilterExpression | None,
- vector_store_provider: VectorStoreProvider | None,
timings: dict[str, float],
) -> tuple[list[dict], list[float] | None]:
t0 = time.monotonic()
@@ -67,18 +64,16 @@ async def hybrid_search(
query_embedding=query_embedding,
top_k=fusion_pool,
filters=filter_expr,
- provider=vector_store_provider,
)
keyword_task = vector_store.text_search(
collection=collection_name,
query_terms=query_terms,
top_k=fusion_pool,
filters=filter_expr,
- provider=vector_store_provider,
)
try:
semantic_results, keyword_raw = await asyncio.gather(semantic_task, keyword_task)
- except VectorStoreFeatureError as exc:
+ except RuntimeError as exc:
raise ValidationError(str(exc)) from exc
timings["search_ms"] = (time.monotonic() - t0) * 1000
@@ -102,7 +97,6 @@ async def semantic_search(
embedding_model: EmbeddingModel,
top_k: int,
filter_expr: FilterExpression | None,
- vector_store_provider: VectorStoreProvider | None,
timings: dict[str, float],
) -> tuple[list[dict], list[float]]:
t0 = time.monotonic()
@@ -115,7 +109,6 @@ async def semantic_search(
query_embedding=query_embedding,
top_k=top_k,
filters=filter_expr,
- provider=vector_store_provider,
)
timings["search_ms"] = (time.monotonic() - t0) * 1000
return results, query_embedding
diff --git a/api/bigrag/services/retrieval/orchestrate.py b/api/bigrag/services/retrieval/orchestrate.py
index 6ed442b1..55006a8d 100644
--- a/api/bigrag/services/retrieval/orchestrate.py
+++ b/api/bigrag/services/retrieval/orchestrate.py
@@ -19,7 +19,6 @@
from bigrag.services.retrieval.modes import hybrid_search, keyword_search, semantic_search
from bigrag.services.retrieval.rerank import rerank_results
from bigrag.services.runtime_settings import get_values
-from bigrag.services.vector_store import VectorStoreProvider, vector_store
logger = get_logger("bigrag.retrieval")
@@ -40,10 +39,6 @@ def _on_done(t: asyncio.Task) -> None:
return task
-def _supports_text_search(provider: VectorStoreProvider | None) -> bool:
- return vector_store.supports_text_search_for(provider)
-
-
async def retrieve(
collection_name: str,
query: str,
@@ -54,7 +49,6 @@ async def retrieve(
search_mode: str = "semantic",
reranking_config: dict | None = None,
rerank_override: bool | None = None,
- vector_store_provider: VectorStoreProvider | None = None,
) -> RetrievalOutcome:
if top_k > MAX_TOP_K:
raise ValidationError(f"top_k {top_k} exceeds maximum {MAX_TOP_K}")
@@ -75,9 +69,6 @@ async def retrieve(
filter_expr = build_filter(filters) if filters else None
except ValueError as exc:
raise ValidationError(str(exc)) from exc
- if search_mode in {"keyword", "hybrid"} and not _supports_text_search(vector_store_provider):
- provider_label = vector_store_provider or vector_store.provider
- raise ValidationError(f"{provider_label} does not support {search_mode} search in v1")
query_terms = tokenize_query(query)
result_cache_key: str | None = None
@@ -126,7 +117,6 @@ async def retrieve(
query_terms=query_terms,
top_k=top_k,
filter_expr=filter_expr,
- vector_store_provider=vector_store_provider,
timings=timings,
)
elif search_mode == "hybrid":
@@ -138,7 +128,6 @@ async def retrieve(
top_k=top_k,
max_top_k=MAX_TOP_K,
filter_expr=filter_expr,
- vector_store_provider=vector_store_provider,
timings=timings,
)
else:
@@ -148,7 +137,6 @@ async def retrieve(
embedding_model=embedding_model,
top_k=top_k,
filter_expr=filter_expr,
- vector_store_provider=vector_store_provider,
timings=timings,
)
@@ -225,7 +213,6 @@ async def retrieve_multi(
search_mode: str = "semantic",
reranking_configs: dict[str, dict] | None = None,
rerank_override: bool | None = None,
- vector_store_providers: dict[str, VectorStoreProvider] | None = None,
) -> list[dict]:
if top_k > MAX_TOP_K:
raise ValidationError(f"top_k {top_k} exceeds maximum {MAX_TOP_K}")
@@ -242,7 +229,6 @@ async def search_one(col_name: str) -> list[dict]:
search_mode=search_mode,
reranking_config=col_reranking,
rerank_override=rerank_override,
- vector_store_provider=(vector_store_providers or {}).get(col_name),
)
for r in outcome.results:
r["collection"] = col_name
diff --git a/api/bigrag/services/vector_store/__init__.py b/api/bigrag/services/vector_store/__init__.py
index dd050983..a171a352 100644
--- a/api/bigrag/services/vector_store/__init__.py
+++ b/api/bigrag/services/vector_store/__init__.py
@@ -1,16 +1,10 @@
from __future__ import annotations
-from bigrag.services.vector_store.base import (
- VectorStoreBackend,
- VectorStoreFeatureError,
- VectorStoreProvider,
-)
+from bigrag.services.vector_store.base import VectorStoreBackend
from bigrag.services.vector_store.facade import VectorStore, vector_store
__all__ = [
"VectorStore",
"VectorStoreBackend",
- "VectorStoreFeatureError",
- "VectorStoreProvider",
"vector_store",
]
diff --git a/api/bigrag/services/vector_store/_util.py b/api/bigrag/services/vector_store/_util.py
deleted file mode 100644
index 20c39cf4..00000000
--- a/api/bigrag/services/vector_store/_util.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from __future__ import annotations
-
-from typing import cast
-
-from bigrag.logging import get_logger
-from bigrag.services.vector_store.base import VectorStoreBackend, VectorStoreProvider
-
-logger = get_logger("bigrag.vector_store")
-
-PROVIDERS: tuple[VectorStoreProvider, ...] = ("turbopuffer",)
-
-
-def validate_provider(value: str) -> VectorStoreProvider:
- if value not in PROVIDERS:
- raise ValueError(f"Unsupported vector store provider: {value}")
- return cast(VectorStoreProvider, value)
-
-
-async def close_backends(
- backends: dict[VectorStoreProvider, VectorStoreBackend],
- *,
- log_errors: bool = False,
-) -> None:
- seen: set[int] = set()
- for backend in backends.values():
- ident = id(backend)
- if ident in seen:
- continue
- seen.add(ident)
- try:
- await backend.close()
- except Exception as exc:
- if log_errors:
- logger.warning("old vector store close failed", error=str(exc))
- else:
- raise
diff --git a/api/bigrag/services/vector_store/base.py b/api/bigrag/services/vector_store/base.py
index d59f98de..ffbda9af 100644
--- a/api/bigrag/services/vector_store/base.py
+++ b/api/bigrag/services/vector_store/base.py
@@ -2,12 +2,10 @@
import uuid
from collections.abc import AsyncIterator
-from typing import Literal, Protocol
+from typing import Protocol
from bigrag.services._retrieval_filters import FilterExpression
-VectorStoreProvider = Literal["turbopuffer"]
-
_POINT_NAMESPACE = uuid.UUID("1b04f7ca-0c3b-5d76-a5bb-6e4b4a40f61d")
_FIXED_PAYLOAD_FIELDS = {"id", "text", "document_id", "chunk_index", "embedding"}
DEFAULT_SEARCH_PAYLOAD_FIELDS: list[str] = [
@@ -23,13 +21,7 @@
]
-class VectorStoreFeatureError(RuntimeError):
- pass
-
-
class VectorStoreBackend(Protocol):
- provider: VectorStoreProvider
-
def connect(self) -> None: ...
async def close(self) -> None: ...
@@ -40,7 +32,6 @@ async def create_collection(
self,
name: str,
dimension: int,
- index_type: str = "HNSW",
tenant_field: str | None = None,
) -> None: ...
diff --git a/api/bigrag/services/vector_store/facade.py b/api/bigrag/services/vector_store/facade.py
index b02daed4..1271a52a 100644
--- a/api/bigrag/services/vector_store/facade.py
+++ b/api/bigrag/services/vector_store/facade.py
@@ -7,7 +7,6 @@
from bigrag.config import settings as _app_settings
from bigrag.services._retrieval_filters import FilterExpression
-from bigrag.services.vector_store._util import close_backends
from bigrag.services.vector_store.base import VectorStoreBackend
from bigrag.services.vector_store.turbopuffer import TurbopufferVectorStore
@@ -25,6 +24,9 @@ def __init__(self) -> None:
self._active = 0
self._swapping = False
+ def is_configured(self) -> bool:
+ return bool(getattr(self.backend, "api_key", None))
+
@property
def backend(self) -> VectorStoreBackend:
return self._backend_instance
@@ -75,7 +77,10 @@ async def replace_with(self, other: VectorStore) -> None:
old_backend = self.backend
self.backend = other.backend
self._sync_client()
- await close_backends({"turbopuffer": old_backend}, log_errors=True)
+ try:
+ await old_backend.close()
+ except Exception:
+ pass
self._swapping = False
self._condition.notify_all()
@@ -112,17 +117,14 @@ async def create_collection(
self,
name: str,
dimension: int,
- index_type: str = "HNSW",
tenant_field: str | None = None,
- **_: Any,
) -> None:
async with self._backend() as backend:
- await backend.create_collection(name, dimension, index_type, tenant_field)
+ await backend.create_collection(name, dimension, tenant_field)
async def delete_collection(
self,
name: str,
- **_: Any,
) -> None:
async with self._backend() as backend:
await backend.delete_collection(name)
@@ -136,7 +138,6 @@ async def insert(
texts: list[str],
embeddings: list[list[float]],
metadata: list[dict] | None = None,
- **_: Any,
) -> int:
async with self._backend() as backend:
return await backend.insert(
@@ -156,7 +157,6 @@ async def search(
top_k: int = 10,
filters: FilterExpression | None = None,
payload_fields: list[str] | None = None,
- **_: Any,
) -> list[dict]:
async with self._backend() as backend:
return await backend.search(
@@ -173,7 +173,6 @@ async def get_chunks(
document_id: str,
limit: int = 10000,
offset: int = 0,
- **_: Any,
) -> tuple[list[dict], int]:
async with self._backend() as backend:
return await backend.get_chunks(collection, document_id, limit, offset)
@@ -182,7 +181,6 @@ async def delete_by_document(
self,
collection: str,
document_id: str,
- **_: Any,
) -> None:
async with self._backend() as backend:
await backend.delete_by_document(collection, document_id)
@@ -191,7 +189,6 @@ async def delete_by_ids(
self,
collection: str,
ids: list[str],
- **_: Any,
) -> None:
async with self._backend() as backend:
await backend.delete_by_ids(collection, ids)
@@ -202,7 +199,6 @@ async def text_search(
query_terms: list[str],
top_k: int = 10,
filters: FilterExpression | None = None,
- **_: Any,
) -> list[dict]:
async with self._backend() as backend:
return await backend.text_search(collection, query_terms, top_k, filters)
@@ -214,7 +210,6 @@ async def upsert(
embeddings: list[list[float]],
texts: list[str],
metadata: list[dict] | None = None,
- **_: Any,
) -> int:
async with self._backend() as backend:
return await backend.upsert(collection, ids, embeddings, texts, metadata)
@@ -224,7 +219,6 @@ async def export_collection_points(
collection: str,
*,
with_vectors: bool = True,
- **_: Any,
) -> list[dict]:
async with self._backend() as backend:
return await backend.export_collection_points(collection, with_vectors=with_vectors)
diff --git a/api/bigrag/services/vector_store/turbopuffer.py b/api/bigrag/services/vector_store/turbopuffer.py
index 5d25966e..aa30e01f 100644
--- a/api/bigrag/services/vector_store/turbopuffer.py
+++ b/api/bigrag/services/vector_store/turbopuffer.py
@@ -7,7 +7,6 @@
from bigrag.logging import get_logger
from bigrag.services._retrieval_filters import FilterCondition, FilterExpression
from bigrag.services.vector_store.base import (
- VectorStoreProvider,
_backend_name,
_build_payload,
_chunk_rows_from_payloads,
@@ -67,8 +66,6 @@ def _row_payload(row: dict) -> dict:
class TurbopufferVectorStore:
- provider: VectorStoreProvider = "turbopuffer"
-
def __init__(
self,
*,
@@ -120,7 +117,6 @@ async def create_collection(
self,
name: str,
dimension: int,
- index_type: str = "HNSW",
tenant_field: str | None = None,
) -> None:
await self.health_check()
diff --git a/bigrag.toml b/bigrag.toml
index 249d3a8f..0a150487 100644
--- a/bigrag.toml
+++ b/bigrag.toml
@@ -21,13 +21,10 @@
# db_pool_max = 50
# migration_timeout_seconds = 60
-# qdrant_url = "http://localhost:6333"
-# qdrant_connect_timeout_seconds = 10
-# qdrant_required = false
-
-# turbopuffer_api_key = "" # required only for collections using turbopuffer
+# turbopuffer_api_key = "" # required for Turbopuffer-backed search
# turbopuffer_region = "aws-us-east-1"
# turbopuffer_namespace_prefix = "bigrag_"
+# turbopuffer_base_url = "" # optional Turbopuffer-compatible endpoint
# redis_url = "redis://localhost:6379/0"
diff --git a/e2e/tests/api/test_health.py b/e2e/tests/api/test_health.py
index a573d1f4..6cbb6711 100644
--- a/e2e/tests/api/test_health.py
+++ b/e2e/tests/api/test_health.py
@@ -31,10 +31,6 @@ async def test_health_readiness_reports_each_dependency(
for dep in ("postgres", "redis", "vector_store", "embedding"):
assert dep in body, f"missing dependency key {dep!r} in {body!r}"
assert isinstance(body[dep], bool)
- assert "vector_store_provider" in body
- assert body["vector_store_provider"] == "turbopuffer"
- assert "turbopuffer" in body
- assert "qdrant" not in body
async def test_health_readiness_status_matches_http_code(
diff --git a/e2e/tests/api/test_settings.py b/e2e/tests/api/test_settings.py
index 6df7f6df..3dc3038a 100644
--- a/e2e/tests/api/test_settings.py
+++ b/e2e/tests/api/test_settings.py
@@ -81,11 +81,11 @@ async def test_settings_get_returns_specs_and_values(
"max_upload_size_mb",
"turbopuffer_api_key",
"turbopuffer_base_url",
+ "turbopuffer_namespace_prefix",
+ "turbopuffer_region",
):
assert sample in spec_keys, f"expected setting key {sample!r} in spec list"
assert sample in body["values"], f"missing value entry for {sample!r}"
- assert "qdrant_url" not in spec_keys
- assert "qdrant_required" not in spec_keys
for spec in body["specs"]:
for key in ("key", "group", "label", "description", "kind", "default", "options", "secret"):
diff --git a/e2e/tests/api/test_vector_storage.py b/e2e/tests/api/test_vector_storage.py
index d14b7a08..e9b2d40c 100644
--- a/e2e/tests/api/test_vector_storage.py
+++ b/e2e/tests/api/test_vector_storage.py
@@ -35,14 +35,11 @@ async def test_vector_storage_overview_returns_provider_shape(
resp = await admin_client.get("/v1/admin/vector-storage/overview")
body = assert_envelope(resp, 200)
- for key in ("provider", "health", "collections", "totals"):
- assert key in body, f"missing top-level key {key!r} in {body!r}"
+ assert set(body) == {"provider", "health", "collections", "totals"}
assert body["provider"] == "turbopuffer"
assert isinstance(body["health"], dict)
assert body["health"].get("status") in {"ok", "error"}
- for removed_key in ("fallback_provider", "configured_providers", "provider_health"):
- assert removed_key not in body
assert isinstance(body["collections"], list)
totals = body["totals"]
diff --git a/website/components/home/code-section.tsx b/website/components/home/code-section.tsx
index c6fa7207..9f28b862 100644
--- a/website/components/home/code-section.tsx
+++ b/website/components/home/code-section.tsx
@@ -39,7 +39,7 @@ export const CodeSection = () => (
- Qdrant vector database with HNSW search and cosine similarity
+ Turbopuffer vector search with semantic, keyword, and hybrid modes
diff --git a/website/content/docs/api-reference/admin-realtime.mdx b/website/content/docs/api-reference/admin-realtime.mdx
index a7ee45cc..9bb38781 100644
--- a/website/content/docs/api-reference/admin-realtime.mdx
+++ b/website/content/docs/api-reference/admin-realtime.mdx
@@ -48,14 +48,13 @@ Connector source streams accept `collection`. Sync-job streams accept `collectio
| `GET /v1/admin/realtime/access/logs` | `GET /v1/admin/access/logs` |
| `GET /v1/admin/realtime/audit` | `GET /v1/admin/audit` |
| `GET /v1/admin/realtime/backups` | `GET /v1/admin/backups` |
-| `GET /v1/admin/realtime/vector-migrations` | `GET /v1/admin/vector-storage/migrations` |
| `GET /v1/admin/realtime/usage` | `GET /v1/usage` |
| `GET /v1/admin/realtime/platform/stats` | `GET /v1/stats` |
| `GET /v1/admin/realtime/platform/readiness` | `GET /health/ready` |
These streams support the same query parameters as their REST counterparts. Where no domain event source exists yet, the backend emits full snapshots from a server-side refresh tick so browsers do not need to poll.
-Backup streams refresh quickly while jobs are pending or running. Vector migration streams refresh quickly while jobs are pending, running, or canceling so stop-and-delete progress is visible.
+Backup streams refresh quickly while jobs are pending or running.
## API Client Fallback
diff --git a/website/content/docs/api-reference/backups.mdx b/website/content/docs/api-reference/backups.mdx
index 8aab7f02..ccb416c3 100644
--- a/website/content/docs/api-reference/backups.mdx
+++ b/website/content/docs/api-reference/backups.mdx
@@ -70,15 +70,15 @@ Content-Type: application/json
Creates a backup job and enqueues it on the Dramatiq backup worker. Only one backup can be pending or running at a time.
-While the job runs, bigRAG acquires a maintenance lock. Mutating API requests are rejected with `423`, new ingestion jobs are rejected, Dramatiq ingestion and connector actors delay themselves, and scheduled Google Drive syncs do not start. Backups and vector migrations share the same maintenance lane, so a pending or running backup blocks new vector migrations and a pending, running, or canceling vector migration blocks new backups.
+While the job runs, bigRAG acquires a maintenance lock. Mutating API requests are rejected with `423`, new ingestion jobs are rejected, Dramatiq ingestion and connector actors delay themselves, and scheduled Google Drive syncs do not start.
The backup fails if:
- The backup bucket settings are incomplete or invalid.
-- A configured vector-store client cannot be reached.
+- Turbopuffer cannot be reached.
- A stored upload file referenced by a document row is missing.
-Starting a backup returns `409` when another backup, vector migration, or maintenance lock is active.
+Starting a backup returns `409` when another backup or maintenance lock is active.
## Realtime stream
diff --git a/website/content/docs/api-reference/collections.mdx b/website/content/docs/api-reference/collections.mdx
index e78ae42c..84b1dac3 100644
--- a/website/content/docs/api-reference/collections.mdx
+++ b/website/content/docs/api-reference/collections.mdx
@@ -32,12 +32,10 @@ GET /v1/collections
"description": "Academic research papers",
"embedding_provider": "openai",
"embedding_model": "text-embedding-3-small",
- "vector_store_provider": "qdrant",
"dimension": 1536,
"chunk_size": 512,
"chunk_overlap": 50,
"chunk_strategy": "paragraph",
- "index_type": "HNSW",
"tenant_field": null,
"has_metadata_schema": false,
"document_count": 15,
@@ -74,12 +72,10 @@ POST /v1/collections
"description": "Academic research papers",
"embedding_provider": "openai",
"embedding_model": "text-embedding-3-small",
- "vector_store_provider": "qdrant",
"dimension": 1536,
"chunk_size": 512,
"chunk_overlap": 50,
"chunk_strategy": "paragraph",
- "index_type": "HNSW",
"tenant_field": "tenant_id",
"metadata_schema": {
"type": "object",
@@ -102,7 +98,6 @@ POST /v1/collections
| `description` | string | no | `""` | — |
| `embedding_provider` | string | no | Server default | `openai`, `cohere`, `voyage`, `openai_compatible` |
| `embedding_model` | string | no | Server default | Model name |
-| `vector_store_provider` | string | no | `"qdrant"` | `qdrant` or `turbopuffer`; changed only through a vector migration job. `turbopuffer` requires a saved turbopuffer API key in Vector Storage before creation or migration |
| `embedding_preset_id` | string | no | — | Link the collection to a saved embedding preset. The collection inherits the preset's provider, model, dimension, base URL, and key live |
| `embedding_api_key` | string | no | — | Required when `embedding_preset_id` is not provided; validated against the provider before the collection is created. Use any non-empty value for local OpenAI-compatible gateways without auth. Ignored when a preset is supplied — the collection inherits the preset's key live |
| `embedding_base_url` | string | no | — | Required for `openai_compatible` (Ollama, vLLM, TEI, LiteLLM, Azure, Bedrock) |
@@ -110,8 +105,7 @@ POST /v1/collections
| `chunk_size` | integer | no | `512` | 64–10,000 |
| `chunk_overlap` | integer | no | `50` | 0–5,000, must be `< chunk_size` |
| `chunk_strategy` | string | no | `"paragraph"` | `paragraph` (split on blank lines) or `recursive` (hierarchical) |
-| `index_type` | string | no | `"HNSW"` | Dense-vector index preference |
-| `tenant_field` | string | no | — | Metadata key indexed by supported vector stores and required on uploads, raw vector upserts, query filters, and chat filters |
+| `tenant_field` | string | no | — | Metadata key required on uploads, raw vector upserts, query filters, and chat filters |
| `metadata_schema` | object | no | — | JSON Schema validated on every upload / upsert |
| `default_top_k` | integer | no | `10` | 1–1,000 |
| `default_min_score` | float | no | `null` | Minimum similarity score |
@@ -127,8 +121,8 @@ POST /v1/collections
**Errors:**
-- `400` — Invalid name, chunk config, dimension mismatch, or unconfigured vector provider
-- `502` — Selected vector provider rejected collection provisioning
+- `400` — Invalid name, chunk config, or dimension mismatch
+- `502` — Vector store rejected collection provisioning
- `409` — Collection name already exists
## Get Collection
@@ -166,7 +160,7 @@ Lightweight endpoint returning document and chunk counts.
PUT /v1/collections/{name}
```
-Only the fields below are mutable. Embedding provider / model / dimension, chunk size / overlap, `index_type`, and `tenant_field` are fixed at creation. Use `POST /v1/admin/vector-storage/migrations` to change a collection's vector store provider.
+Only the fields below are mutable. Embedding provider / model / dimension, chunk size / overlap, and `tenant_field` are fixed at creation.
| Field | Type | Notes |
|-------|------|-------|
diff --git a/website/content/docs/api-reference/health.mdx b/website/content/docs/api-reference/health.mdx
index f196c1df..6324606c 100644
--- a/website/content/docs/api-reference/health.mdx
+++ b/website/content/docs/api-reference/health.mdx
@@ -21,7 +21,7 @@ No authentication required. Returns 200 if the server is running.
GET /health/ready
```
-No authentication required. Tests connectivity to Postgres, configured vector-store clients, Redis, and the configured embedding provider.
+No authentication required. Tests connectivity to Postgres, Turbopuffer, Redis, and the configured embedding provider.
**Response** `200`:
@@ -31,8 +31,6 @@ No authentication required. Tests connectivity to Postgres, configured vector-st
"version": "2026.4.30",
"postgres": true,
"vector_store": true,
- "vector_store_provider": "per_collection",
- "qdrant": true,
"redis": true,
"embedding": true,
"embedding_source": "settings"
@@ -47,8 +45,6 @@ When a dependency is unreachable or misconfigured:
"version": "2026.4.30",
"postgres": true,
"vector_store": true,
- "vector_store_provider": "per_collection",
- "qdrant": true,
"redis": false,
"redis_error": "unreachable",
"embedding": false,
diff --git a/website/content/docs/api-reference/instance-settings.mdx b/website/content/docs/api-reference/instance-settings.mdx
index 409bdf37..0a987635 100644
--- a/website/content/docs/api-reference/instance-settings.mdx
+++ b/website/content/docs/api-reference/instance-settings.mdx
@@ -16,7 +16,7 @@ The Settings UI is optimized for day-to-day operators. It does not show every re
| `/settings → Security` | Trusted proxy ranges, outbound provider URL policy, private-network escape hatches, local webhook policy, and embedding-cache posture. |
| `/data-storage` | Uploaded source-file storage for local disk and S3-compatible object stores. |
| Deployment config or this API | CORS origins and session-cookie flags: `cors_origins`, `session_cookie_secure`, `session_cookie_samesite`, and `session_cookie_domain`. These can lock admins out when changed incorrectly, so they are best handled deliberately during deployment. |
-| Other Settings pages | Ingestion, queue, retention, webhooks, model defaults, vector storage, and backups. |
+| Other Settings pages | Ingestion, queue, retention, webhooks, model defaults, Turbopuffer vector storage, and backups. |
## List Settings
@@ -101,9 +101,9 @@ Deployment-managed security keys use the same endpoint when you intentionally au
}
```
-Security hardening settings include `allow_public_bind_in_prod`. Ingestion settings include raw vector API request caps (`max_vector_upsert_count`, `max_vector_delete_count`, `max_vector_text_chars`, `max_vector_metadata_bytes`). UI-visible settings can be changed from the admin UI. Registry keys that are not rendered in the UI remain available through this API and the SDK admin settings resources.
+Security hardening settings include `allow_public_bind_in_prod`. Ingestion settings include raw vector API request caps (`max_vector_upsert_count`, `max_vector_delete_count`, `max_vector_text_chars`, `max_vector_metadata_bytes`). Turbopuffer settings include `turbopuffer_api_key`, `turbopuffer_base_url`, `turbopuffer_region`, and `turbopuffer_namespace_prefix`. UI-visible settings can be changed from the admin UI. Registry keys that are not rendered in the UI remain available through this API and the SDK admin settings resources.
-Storage and vector-store connection changes are validated before they are saved. Existing collections keep their current provider until an admin starts a vector migration job.
+Storage and Turbopuffer connection changes are validated before they are saved and apply to the running API.
## Test Settings
@@ -111,7 +111,7 @@ Storage and vector-store connection changes are validated before they are saved.
POST /v1/admin/settings/test
```
-Validates the submitted values without saving them. Storage settings run a lightweight backend construction/probe when S3 is selected. Backup settings run a lightweight S3-compatible bucket probe. Save operations use the same validation path for changed keys; this endpoint remains useful for SDKs and automation that want a dry run.
+Validates the submitted values without saving them. Storage settings run a lightweight backend construction/probe when S3 is selected. Backup settings run a lightweight S3-compatible bucket probe. Turbopuffer settings run the same connectivity validation used by save operations. This endpoint remains useful for SDKs and automation that want a dry run.
```json
{ "values": { "storage_backend": "s3", "storage_s3_bucket": "bigrag-documents" } }
@@ -121,6 +121,10 @@ Validates the submitted values without saving them. Storage settings run a light
{ "values": { "backup_s3_bucket": "bigrag-backups", "backup_s3_endpoint_url": "https://ACCOUNT.r2.cloudflarestorage.com", "backup_s3_region": "auto" } }
```
+```json
+{ "values": { "turbopuffer_api_key": "tpuf_...", "turbopuffer_region": "aws-us-east-1", "turbopuffer_namespace_prefix": "prod_" } }
+```
+
## Reset Settings
```
diff --git a/website/content/docs/api-reference/vectors.mdx b/website/content/docs/api-reference/vectors.mdx
index 2f397295..6bb26936 100644
--- a/website/content/docs/api-reference/vectors.mdx
+++ b/website/content/docs/api-reference/vectors.mdx
@@ -15,19 +15,19 @@ Scoped API keys need `vector:write` for upsert and `vector:delete` for delete. `
GET /v1/admin/vector-storage/overview
```
-Session-only. Returns configured vector providers, provider health, and per-collection routing/stats for the admin UI `/vector-storage` page.
+Session-only. Returns Turbopuffer health plus collection and vector totals for admin diagnostics.
```json
{
- "fallback_provider": "collection",
- "configured_providers": ["qdrant", "turbopuffer"],
"provider_health": {
- "qdrant": { "configured": true, "status": "ok", "error": null }
+ "name": "turbopuffer",
+ "configured": true,
+ "status": "ok",
+ "error": null
},
"collections": [
{
"name": "docs",
- "provider": "qdrant",
"documents": 12,
"chunks": 480,
"bytes": 1048576
@@ -37,66 +37,6 @@ Session-only. Returns configured vector providers, provider health, and per-coll
}
```
-## Admin Vector Migrations
-
-```
-GET /v1/admin/vector-storage/migrations
-POST /v1/admin/vector-storage/migrations
-GET /v1/admin/vector-storage/migrations/{migration_id}
-DELETE /v1/admin/vector-storage/migrations/{migration_id}
-GET /v1/admin/realtime/vector-migrations
-```
-
-Session-only. Migrates one collection in place from its current vector provider to a configured target provider.
-
-`GET /v1/admin/vector-storage/migrations` accepts:
-
-| Name | Type | Default | Notes |
-|------|------|---------|-------|
-| `collection` | string | — | Filter jobs to one collection |
-| `limit` | integer | `20` | 1-100 |
-| `offset` | integer | `0` | Offset pagination when `cursor` is omitted |
-| `cursor` | string | — | Cursor returned as `next_cursor` |
-| `include_total` | boolean | `false` | Include the total job count for the filter |
-
-`POST /v1/admin/vector-storage/migrations` starts a background maintenance job:
-
-```json
-{
- "collection": "docs",
- "target_provider": "turbopuffer"
-}
-```
-
-The job pauses writes with the maintenance lock, waits for active ingestion and connector sync work to drain, copies provider-neutral vector points, switches `vector_store_provider`, invalidates collection/search caches, and deletes the old source provider collection after a successful cutover. Backups and vector migrations share the same maintenance lane, so a pending or running backup blocks new vector migrations and a pending, running, or canceling vector migration blocks new backups.
-
-`DELETE /v1/admin/vector-storage/migrations/{migration_id}` deletes completed or failed migration records. If the job is pending, running, or canceling, it requests a stop first and removes the record after the worker has cleaned up any partial target collection. A migration that has already entered cutover finishes cleanup before removal. This endpoint remains available while the maintenance lock is active so operators can stop a running migration.
-
-**Response** `201`:
-
-```json
-{
- "id": "0196f3c9-8f45-7d39-9ad3-cfc3f34bd23d",
- "collection_name": "docs",
- "source_provider": "qdrant",
- "target_provider": "turbopuffer",
- "status": "pending",
- "phase": "queued",
- "progress": 0,
- "copied_points": 0,
- "total_points": null,
- "error_message": null
-}
-```
-
-`GET /v1/admin/realtime/vector-migrations` is an SSE stream whose `snapshot` payload matches the list response. Add `?collection=docs` to scope the stream to one collection. Pending, running, and canceling jobs keep the stream on the short refresh interval.
-
-**Errors:**
-
-- `400` — Collection missing or target provider not configured
-- `409` — The collection already uses the target provider, another migration or backup is active, or another maintenance lock is active
-- `404` — Migration job not found
-
## Upsert Vectors
```
diff --git a/website/content/docs/comparison.mdx b/website/content/docs/comparison.mdx
index 723c4980..93018ba2 100644
--- a/website/content/docs/comparison.mdx
+++ b/website/content/docs/comparison.mdx
@@ -22,7 +22,7 @@ There are many ways to build a RAG pipeline. This page compares bigRAG to popula
| **Python SDK** | Yes | Yes | No | No | No | Yes | Yes | Yes | Yes |
| **Web UI** | Yes (admin UI) | Yes | Yes | Yes | Yes | No | No | No | Yes |
| **LLM generation** | Yes (chat API) | Yes | Yes | Yes | Yes | Via code | Via code | Via code | Yes |
-| **Vector DB** | Qdrant default, turbopuffer | OpenSearch | Elasticsearch | 9 options | Qdrant or pgvector | Integrations | Integrations | Integrations | Proprietary |
+| **Vector DB** | Turbopuffer | OpenSearch | Elasticsearch | 9 options | Local vector DB or pgvector | Integrations | Integrations | Integrations | Proprietary |
| **Multi-collection** | Yes | No | Yes | Yes | No | N/A | N/A | N/A | Yes |
| **Setup complexity** | Low | Low | Medium | Low | Medium | High | High | High | None |
@@ -68,7 +68,7 @@ These are the closest alternatives to bigRAG — self-hostable platforms that ha
**Where bigRAG is a better fit:**
- Lighter footprint — RAGFlow requires 4+ cores and 16 GB RAM minimum
- API-first design for developers building integrations, not end-user apps
-- Purpose-built vector storage: Qdrant by default, with turbopuffer as a managed cloud backend
+- Purpose-built Turbopuffer storage for semantic, keyword, and hybrid search
- Webhook-driven architecture for event-based workflows
- Simpler deployment and configuration
diff --git a/website/content/docs/concepts/architecture.mdx b/website/content/docs/concepts/architecture.mdx
index 3536cd83..33e701f9 100644
--- a/website/content/docs/concepts/architecture.mdx
+++ b/website/content/docs/concepts/architecture.mdx
@@ -10,9 +10,8 @@ import { Callout } from "fumadocs-ui/components/callout";
bigRAG intentionally keeps four distinct stores instead of shoving
everything into Postgres with pgvector. Each one earns its keep:
-- **Vector store** — Qdrant by default, with turbopuffer available as an
- instance-level managed backend. The vector store owns dense embeddings,
- payload metadata, and filtered semantic search.
+- **Vector store** — Turbopuffer owns dense embeddings, chunk text,
+ payload metadata, BM25 full-text search, and filtered semantic search.
- **Postgres** — metadata and control plane. Collections, documents,
users, API keys, webhooks, query logs, audit trail, embedding cache.
Transactions keep ingestion consistent across stores.
@@ -26,9 +25,8 @@ everything into Postgres with pgvector. Each one earns its keep:
re-uploading.
-bigRAG can run entirely from `docker compose up` on a single machine
-with Qdrant. Turbopuffer moves vector operations out of the local
-deployment when you prefer a managed vector service.
+bigRAG runs Postgres and Redis locally with `docker compose up`, while
+Turbopuffer provides the vector and full-text search backend.
The admin UI leans on TanStack Router's built-in route code splitting
@@ -56,8 +54,8 @@ identifiers such as Google Drive file IDs, caller-provided vector IDs,
collection names, settings keys, idempotency keys, OAuth states, session
tokens, API keys, and webhook secrets remain opaque strings. Vector-store
point IDs are backend-safe deterministic UUIDv5 values derived from the
-caller-facing vector or chunk ID, so Qdrant and turbopuffer can use stable
-point IDs without changing API-visible IDs.
+caller-facing vector or chunk ID, so Turbopuffer writes, deletes, and
+exports can use stable point IDs without changing API-visible IDs.
## Request flow — `POST /v1/collections/{name}/documents`
@@ -99,7 +97,7 @@ Client → FastAPI
│ 1. auth → scope check (query:read)
│ 2. embed query when search mode needs vectors
│ 3. vector search (with metadata filters)
- │ 4. optional hybrid fusion — Qdrant keyword search + reciprocal rank fusion
+ │ 4. optional hybrid fusion — Turbopuffer BM25 + reciprocal rank fusion
│ 5. optional rerank — Cohere Rerank v3.5
│ 6. attach timings (embed/search/rerank/cache/total)
▼
@@ -107,9 +105,9 @@ Response JSON with results and timings
```
Keyword mode skips embeddings. Semantic and hybrid mode use the
-collection's configured embedding model. In v1, keyword and hybrid
-search are available on Qdrant; turbopuffer supports semantic search
-and returns a clear error for keyword or hybrid mode.
+collection's configured embedding model. Keyword search runs BM25 over
+Turbopuffer full-text chunk data, and hybrid search fuses Turbopuffer
+ANN results with BM25 results using reciprocal rank fusion.
Query-result cache hits return the cached chunks but report the current
cache lookup latency, not the original uncached retrieval timings.
@@ -128,25 +126,24 @@ images, and exposes layout provenance we can thread into citation metadata
(`page_no`, `bbox`). PDFs with embedded text use a faster direct extractor;
local OCR for scanned PDFs is enabled by default.
-## Why Qdrant by default
+## Why Turbopuffer
-pgvector is tempting — one less store, one less deploy. But at scale
-the gap widens:
+pgvector is tempting — one less external service. But at scale the gap
+widens:
-- **Vector engine**: Qdrant gives bigRAG a dedicated HNSW vector
- store with payload-aware filtering and operational knobs such as
- search `ef`. pgvector can work well, but it shares capacity and
- tuning with the transactional database.
-- **Payload indexes**: Qdrant can index tenant IDs, document IDs,
- chunk offsets, pages, and text payloads alongside vectors. pgvector
- deployments usually need extra Postgres indexes and careful query
- planning to keep filtered ANN search predictable.
-- **Dedicated memory management**: Qdrant won't blow out Postgres's
- shared buffers during a large ANN scan.
+- **Managed vector engine**: Turbopuffer keeps ANN search and payload
+ filtering outside the transactional database, so Postgres remains the
+ metadata and control plane.
+- **Full-text plus vectors**: Turbopuffer stores chunk text with
+ full-text search enabled, which lets keyword search run as BM25 and
+ hybrid search fuse BM25 with semantic results.
+- **Namespace isolation**: each collection maps to a namespace, keeping
+ collection deletion, truncation, export, and raw vector writes scoped
+ to one backend object.
For single-tenant deployments under ~1M vectors, pgvector would work
-— and the `VectorStore` abstraction in bigRAG leaves a door open for
-a future pgvector backend.
+well. bigRAG keeps Postgres focused on durable metadata and uses
+Turbopuffer for retrieval-specific storage and query execution.
## Scaling notes
@@ -164,11 +161,9 @@ a future pgvector backend.
seeded after boot through the same Redis scheduler guards used by the
actors, so restarts and replicas do not create duplicate delayed
messages.
-- **Vector backend choice**: use local Qdrant for self-hosted installs
- or turbopuffer for a managed vector search service. Turbopuffer
- namespaces are created on first write and store bigRAG vector IDs as
- payload attributes while using backend-safe IDs internally for writes,
- deletes, and exports.
+- **Vector backend**: Turbopuffer namespaces are created on first write
+ and store bigRAG vector IDs as payload attributes while using
+ backend-safe IDs internally for writes, deletes, and exports.
- **Postgres replication**: the control plane is read-light; a
warm-standby suffices for failover.
- **Redis persistence**: enable `appendonly yes` (already set in the
diff --git a/website/content/docs/concepts/collections.mdx b/website/content/docs/concepts/collections.mdx
index 8cec8b13..0c96b0f8 100644
--- a/website/content/docs/concepts/collections.mdx
+++ b/website/content/docs/concepts/collections.mdx
@@ -5,14 +5,13 @@ description: Collections are logical groupings of documents that share the same
import { Callout } from "fumadocs-ui/components/callout";
-A **collection** is a logical grouping of documents that share the same embedding and vector-storage configuration. Each collection maps to one backend object in its selected vector store.
+A **collection** is a logical grouping of documents that share the same embedding configuration. Each collection maps to one Turbopuffer namespace for vector, metadata, and full-text search.
## What is a Collection?
When you create a collection, you define:
- **Embedding provider and model** — how documents will be embedded (e.g., OpenAI `text-embedding-3-small`)
-- **Vector storage provider** — where vectors for this collection live (`qdrant` or `turbopuffer`)
- **Chunk size and overlap** — how documents are split into searchable segments
- **Default query settings** — `top_k`, `min_score`, and `search_mode` defaults
- **Multimodal element handling** — whether ingestion stores headings, tables, equations, images, page bounds, and optional VLM summaries
@@ -30,7 +29,6 @@ curl -X POST http://localhost:4000/v1/collections \
"description": "Academic research papers",
"embedding_provider": "openai",
"embedding_model": "text-embedding-3-small",
- "vector_store_provider": "qdrant",
"embedding_api_key": "sk-...",
"dimension": 1536,
"chunk_size": 512,
@@ -46,7 +44,6 @@ curl -X POST http://localhost:4000/v1/collections \
| `description` | string | no | `""` | Human-readable description |
| `embedding_provider` | string | no | Server default | `openai`, `openai_compatible`, `cohere`, or `voyage` |
| `embedding_model` | string | no | Server default | Model name for the provider |
-| `vector_store_provider` | string | no | `"qdrant"` | `qdrant` or `turbopuffer`; changed only through a vector migration job. `turbopuffer` requires a saved turbopuffer API key in Vector Storage before creation or migration |
| `embedding_api_key` | string | no | — | API key for the embedding provider; use any non-empty value for local OpenAI-compatible gateways without auth |
| `embedding_base_url` | string | no | — | Required for `openai_compatible` providers |
| `dimension` | integer | no | Server default | Embedding vector dimension |
@@ -62,9 +59,7 @@ curl -X POST http://localhost:4000/v1/collections \
| `multimodal_enrichment_enabled` | boolean | no | `false` | Queue VLM summaries for image, table, and equation elements; requires `multimodal_enabled` |
| `metadata` | object | no | `{}` | Arbitrary key-value pairs |
-For turbopuffer collections, creation validates the saved turbopuffer connection, then the provider namespace is created on the first document or vector write.
-
-To move an existing collection between Qdrant and turbopuffer, start a vector migration from `/vector-storage` or `POST /v1/admin/vector-storage/migrations`. The migration pauses writes, copies existing vectors to the configured target provider, switches the collection, and deletes the old source vectors after cutover. Pending, running, or canceling migrations can be stopped and removed from `/vector-storage` or `DELETE /v1/admin/vector-storage/migrations/{migration_id}`. Backups and vector migrations share the same maintenance lane, so only one of them can be pending or running at a time.
+Collection creation validates the saved Turbopuffer connection. The namespace is created on the first document or vector write, and chunk text is stored with full-text search enabled so semantic, keyword, and hybrid retrieval use the same backend.
## Collection Stats
@@ -160,7 +155,7 @@ curl -X PUT http://localhost:4000/v1/collections/research_papers \
All fields are optional — only provided fields are updated.
- Embedding configuration (provider, model, dimension) and chunk settings cannot be changed after creation. Vector storage provider changes must use a vector migration job.
+ Embedding configuration (provider, model, dimension) and chunk settings cannot be changed after creation.
## Deleting a Collection
diff --git a/website/content/docs/concepts/search.mdx b/website/content/docs/concepts/search.mdx
index b5871430..b255c7cc 100644
--- a/website/content/docs/concepts/search.mdx
+++ b/website/content/docs/concepts/search.mdx
@@ -10,7 +10,7 @@ bigRAG exposes three search modes and an optional rerank pass. Keep query-time c
| Mode | Description |
|------|-------------|
| `semantic` | Default. Cosine similarity against stored vectors. |
-| `keyword` | Text-based matching using term frequency. No embedding involved. |
+| `keyword` | Turbopuffer BM25 full-text matching over stored chunk text. No embedding involved. |
| `hybrid` | Runs semantic and keyword in parallel, then merges with reciprocal rank fusion. |
### Semantic
@@ -85,9 +85,9 @@ Pass a plain value for exact match, or use operators for more control.
}
```
-Multiple filters are combined with AND. When a collection was created with `tenant_field`, bigRAG configures that field for backend filtering where supported and requires the tenant field in every query and chat filter. Missing tenant filters return `400`.
+Multiple filters are combined with AND. When a collection was created with `tenant_field`, bigRAG configures that field for backend filtering and requires the tenant field in every query and chat filter. Missing tenant filters return `400`.
-Keyword and hybrid search require the default Qdrant backend in v1. Turbopuffer supports semantic search and returns a clear error for keyword or hybrid mode.
+Keyword search uses Turbopuffer BM25 over the chunk `text` field. Hybrid search runs Turbopuffer ANN and BM25 queries, then merges the two result sets with reciprocal rank fusion before optional reranking.
## Reranking
diff --git a/website/content/docs/cookbook/multi-tenant-saas.mdx b/website/content/docs/cookbook/multi-tenant-saas.mdx
index cdbd25f6..a621662a 100644
--- a/website/content/docs/cookbook/multi-tenant-saas.mdx
+++ b/website/content/docs/cookbook/multi-tenant-saas.mdx
@@ -54,15 +54,15 @@ The middleware enforces both the scopes and the collection pin, so a leaked tena
Collections-per-tenant is simple but creates operational overhead as
tenant count grows. Above small/medium tenant counts, move to Pattern B
-so one Qdrant collection can use indexed tenant payload filters.
+so one Turbopuffer namespace can use tenant metadata filters.
## Pattern B — shared collection with tenant filters
One collection, tenant isolation via a required metadata filter. Setting
-`tenant_field` tells bigRAG to create a Qdrant payload index for that
-field and reject uploads, raw vector upserts, queries, and chat calls that
-omit the tenant field.
+`tenant_field` tells bigRAG to configure that field for backend filtering
+and reject uploads, raw vector upserts, queries, and chat calls that omit
+the tenant field.
```python
await client.collections.create({
diff --git a/website/content/docs/deployment/docker.mdx b/website/content/docs/deployment/docker.mdx
index ce5655e4..293d43b8 100644
--- a/website/content/docs/deployment/docker.mdx
+++ b/website/content/docs/deployment/docker.mdx
@@ -27,7 +27,7 @@ BIGRAG_UI_IMAGE=yoginth/bigrag-ui:2026.4.30 \
docker compose up -d --no-build
```
-This starts bigRAG API, worker, admin UI, Postgres, Redis, and Qdrant.
+This starts bigRAG API, worker, admin UI, Postgres, and Redis. Turbopuffer runs as the managed vector and full-text search backend.
### Verify
@@ -40,7 +40,7 @@ curl http://localhost:4000/health
## Docker Compose
-The default `docker-compose.yml` runs Qdrant as a single service. Qdrant does not require etcd for this standalone setup. When hacking from a checkout, plain `docker compose up -d` builds local API and UI images. When deploying published images, set the image variables shown above and use `--no-build`.
+The default `docker-compose.yml` runs the API, worker, admin UI, Postgres, and Redis. Turbopuffer is configured through environment variables and does not run as a local Compose service. When hacking from a checkout, plain `docker compose up -d` builds local API and UI images. When deploying published images, set the image variables shown above and use `--no-build`.
```yaml
services:
@@ -53,7 +53,10 @@ services:
environment:
BIGRAG_ENV: "${BIGRAG_ENV:-dev}"
BIGRAG_DATABASE_URL: postgres://bigrag:bigrag@postgres:5432/bigrag?sslmode=disable
- BIGRAG_QDRANT_URL: http://qdrant:6333
+ BIGRAG_TURBOPUFFER_API_KEY: "${BIGRAG_TURBOPUFFER_API_KEY:-}"
+ BIGRAG_TURBOPUFFER_REGION: "${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}"
+ BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: "${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}"
+ BIGRAG_TURBOPUFFER_BASE_URL: "${BIGRAG_TURBOPUFFER_BASE_URL:-}"
BIGRAG_REDIS_URL: "${BIGRAG_REDIS_URL:-redis://redis:6379/0}"
BIGRAG_MASTER_KEY: "${BIGRAG_MASTER_KEY:-}"
BIGRAG_MASTER_KEY_PREVIOUS: '${BIGRAG_MASTER_KEY_PREVIOUS:-[]}'
@@ -70,8 +73,6 @@ services:
condition: service_healthy
redis:
condition: service_healthy
- qdrant:
- condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 30s
@@ -86,7 +87,10 @@ services:
environment:
BIGRAG_ENV: "${BIGRAG_ENV:-dev}"
BIGRAG_DATABASE_URL: postgres://bigrag:bigrag@postgres:5432/bigrag?sslmode=disable
- BIGRAG_QDRANT_URL: http://qdrant:6333
+ BIGRAG_TURBOPUFFER_API_KEY: "${BIGRAG_TURBOPUFFER_API_KEY:-}"
+ BIGRAG_TURBOPUFFER_REGION: "${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}"
+ BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: "${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}"
+ BIGRAG_TURBOPUFFER_BASE_URL: "${BIGRAG_TURBOPUFFER_BASE_URL:-}"
BIGRAG_REDIS_URL: "${BIGRAG_REDIS_URL:-redis://redis:6379/0}"
BIGRAG_UPLOAD_DIR: /data/uploads
BIGRAG_MASTER_KEY: "${BIGRAG_MASTER_KEY:-}"
@@ -103,8 +107,6 @@ services:
condition: service_healthy
redis:
condition: service_healthy
- qdrant:
- condition: service_started
healthcheck:
test:
[
@@ -140,14 +142,6 @@ services:
timeout: 5s
retries: 5
- qdrant:
- image: qdrant/qdrant:v1.17.1
- ports:
- - "6333:6333"
- - "6334:6334"
- volumes:
- - qdrant_data:/qdrant/storage
-
redis:
image: redis:7-alpine
environment:
@@ -171,7 +165,6 @@ services:
volumes:
bigrag_data:
postgres_data:
- qdrant_data:
redis_data:
```
@@ -208,8 +201,6 @@ Scale the worker with more `bigrag-worker` containers from the `bigrag-api` imag
|---------|-------------|
| bigRAG API | 4000 |
| PostgreSQL | 5432 |
-| Qdrant HTTP | 6333 |
-| Qdrant gRPC | 6334 |
| Redis | 6379 |
## Health Checks
@@ -219,7 +210,7 @@ bigRAG exposes two health endpoints:
| Endpoint | Purpose | Notes |
|----------|---------|-------|
| `/health` | Liveness check | Always returns `200` if the server is running |
-| `/health/ready` | Readiness check | Returns `503` if Postgres, Redis, configured vector-store clients, or the embedding provider is unreachable |
+| `/health/ready` | Readiness check | Returns `503` if Postgres, Redis, Turbopuffer, or the embedding provider is unreachable |
```bash
# Quick liveness check
@@ -230,7 +221,6 @@ curl http://localhost:4000/health/ready
# Infrastructure services
docker exec bigrag-postgres pg_isready -U bigrag
-curl -f http://localhost:6333/healthz
docker exec bigrag-redis redis-cli ping
```
diff --git a/website/content/docs/deployment/encryption.mdx b/website/content/docs/deployment/encryption.mdx
index d3fb9bb8..c54dc3c4 100644
--- a/website/content/docs/deployment/encryption.mdx
+++ b/website/content/docs/deployment/encryption.mdx
@@ -8,7 +8,7 @@ import { Callout } from "fumadocs-ui/components/callout";
bigRAG treats "at-rest encryption" as a split responsibility:
- **App-layer envelope encryption** (handled by bigRAG) — sensitive credential columns, persistent embedding-cache rows, and Redis cache payloads are Fernet-encrypted before they ever touch disk.
-- **Storage-layer encryption** (handled by the operator) — document chunks, Qdrant vectors and payloads, uploaded files, Redis persistence, and backups still rely on the disk or object-store encrypting its own data.
+- **Storage-layer encryption** (handled by the operator) — document chunks, Turbopuffer vectors and payload attributes, uploaded files, Redis persistence, and backups still rely on the disk, managed service, or object-store encrypting its own data.
Both layers are required for a defensible "encrypted at rest" posture.
@@ -57,10 +57,10 @@ bigRAG delegates the bulk of at-rest protection to your infrastructure. For a pr
- **RDS / Aurora**: enable **Storage encryption** with a customer-managed KMS key. Enable the same on every read replica and snapshot target.
- **Backups**: pipe `pg_dump` into a GPG or `age`-encrypted file, or store it in an encrypted backup system.
-### Qdrant
+### Turbopuffer
-- Qdrant stores vectors, payloads, and indexes under `/qdrant/storage` in the Docker deployment. Put that volume on encrypted storage.
-- For Qdrant Cloud, enable the provider's encryption controls and restrict API keys to the bigRAG service.
+- Turbopuffer stores vectors, full-text fields, payload attributes, and indexes in its managed service. Select the production region deliberately and keep the API key scoped to the bigRAG service.
+- Store `BIGRAG_TURBOPUFFER_API_KEY` in your secrets manager, rotate it on the same cadence as other provider credentials, and restrict outbound network access where your platform allows it.
### Redis
@@ -74,7 +74,7 @@ The upload directory (default `./data/uploads`) must live on an encrypted volume
### Readable backups
-Readable backups created from `/backups` are intentionally not client-side encrypted. They keep logs, Qdrant payloads, and raw uploaded files inspectable, but redact provider keys, OAuth tokens, webhook secrets, S3 credentials, API key hashes, session hashes, and embedding-cache vectors. Put the backup bucket behind strict IAM/R2 token policy, bucket encryption, private networking where available, lifecycle retention, and access logging.
+Readable backups created from `/backups` are intentionally not client-side encrypted. They keep logs, Turbopuffer payload exports, and raw uploaded files inspectable, but redact provider keys, OAuth tokens, webhook secrets, S3 credentials, API key hashes, session hashes, and embedding-cache vectors. Put the backup bucket behind strict IAM/R2 token policy, bucket encryption, private networking where available, lifecycle retention, and access logging.
## Rotation
@@ -94,7 +94,7 @@ A built-in `bigrag crypto rotate` command is not available yet; use a controlled
| Data | Why |
|------|-----|
| Document chunks (`document_chunks.content`) | Per-query decrypt kills search latency; rely on disk encryption. |
-| Vectors and text payloads in Qdrant | Search needs raw floats and filterable payloads, so use Qdrant auth, tenant filters, network isolation, and encrypted storage. |
+| Vectors and text payloads in Turbopuffer | Search needs raw floats and filterable payloads, so use Turbopuffer API-key controls, tenant filters, network isolation, and managed-service encryption. |
| Audit log rows | Append-only. Store encrypted at the disk layer; querying encrypted metadata is prohibitive. |
| User display names / emails | PII, not credentials. Disk-layer + column-level access control is the right split. |
diff --git a/website/content/docs/deployment/production.mdx b/website/content/docs/deployment/production.mdx
index 3c2c3d2d..36e53277 100644
--- a/website/content/docs/deployment/production.mdx
+++ b/website/content/docs/deployment/production.mdx
@@ -22,11 +22,11 @@ Tick these off before pointing real traffic at a bigRAG deployment:
proxies if bigRAG sits behind nginx, Caddy, Traefik, or a load balancer.
- [ ] Redis with `requirepass` set and `appendonly yes` persisted to
a mounted volume.
-- [ ] Qdrant cluster mode or turbopuffer if you expect >10M vectors or need managed HA.
+- [ ] Turbopuffer API key and region configured for managed vector and full-text search.
- [ ] Postgres warm-standby replica for failover.
- [ ] Backup strategy: configure readable S3/R2 backups from `/backups`,
protect that bucket as sensitive, and separately keep encrypted infrastructure
- snapshots for Postgres, vector-store backends, Redis, and uploads.
+ snapshots for Postgres, Turbopuffer exports, Redis, and uploads.
- [ ] Set `metadata_schema` on collections that accept untrusted
metadata so uploads with invalid shape are rejected at the edge.
- [ ] Wire `GET /v1/admin/audit` into your SIEM / log pipeline.
@@ -62,7 +62,10 @@ services:
environment:
BIGRAG_ENV: prod
BIGRAG_DATABASE_URL: postgres://bigrag:strongpassword@postgres:5432/bigrag
- BIGRAG_QDRANT_URL: http://qdrant:6333
+ BIGRAG_TURBOPUFFER_API_KEY: ${BIGRAG_TURBOPUFFER_API_KEY}
+ BIGRAG_TURBOPUFFER_REGION: ${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}
+ BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: ${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}
+ BIGRAG_TURBOPUFFER_BASE_URL: ${BIGRAG_TURBOPUFFER_BASE_URL:-}
BIGRAG_REDIS_URL: ${BIGRAG_REDIS_URL}
BIGRAG_HOST: 0.0.0.0
BIGRAG_MASTER_KEY: ${BIGRAG_MASTER_KEY}
@@ -86,8 +89,6 @@ services:
condition: service_healthy
redis:
condition: service_healthy
- qdrant:
- condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 30s
@@ -102,7 +103,10 @@ services:
environment:
BIGRAG_ENV: prod
BIGRAG_DATABASE_URL: postgres://bigrag:strongpassword@postgres:5432/bigrag
- BIGRAG_QDRANT_URL: http://qdrant:6333
+ BIGRAG_TURBOPUFFER_API_KEY: ${BIGRAG_TURBOPUFFER_API_KEY}
+ BIGRAG_TURBOPUFFER_REGION: ${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}
+ BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: ${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}
+ BIGRAG_TURBOPUFFER_BASE_URL: ${BIGRAG_TURBOPUFFER_BASE_URL:-}
BIGRAG_REDIS_URL: ${BIGRAG_REDIS_URL}
BIGRAG_MASTER_KEY: ${BIGRAG_MASTER_KEY}
BIGRAG_MASTER_KEY_PREVIOUS: '${BIGRAG_MASTER_KEY_PREVIOUS:-[]}'
@@ -120,8 +124,6 @@ services:
condition: service_healthy
redis:
condition: service_healthy
- qdrant:
- condition: service_started
healthcheck:
test:
[
@@ -161,18 +163,6 @@ services:
timeout: 5s
retries: 5
- qdrant:
- image: qdrant/qdrant:v1.17.1
- ports:
- - "127.0.0.1:6333:6333"
- - "127.0.0.1:6334:6334"
- volumes:
- - qdrant_data:/qdrant/storage
- deploy:
- resources:
- limits:
- memory: 2g
-
redis:
image: redis:7-alpine
environment:
@@ -197,7 +187,6 @@ services:
volumes:
bigrag_data:
postgres_data:
- qdrant_data:
redis_data:
```
@@ -230,13 +219,15 @@ REDIS_PASSWORD=
BIGRAG_REDIS_URL=redis://:@redis:6379/0
BIGRAG_MASTER_KEY=
BIGRAG_ALLOW_PUBLIC_BIND_IN_PROD=true # required when the API image binds 0.0.0.0 in prod
+BIGRAG_TURBOPUFFER_API_KEY=
+BIGRAG_TURBOPUFFER_REGION=aws-us-east-1
+BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX=bigrag_
# Performance
BIGRAG_WORKERS=8 # Match CPU cores
BIGRAG_INGESTION_WORKERS=8 # UI/runtime concurrency target for ingestion
BIGRAG_DB_POOL_MAX=100 # Increase for high concurrency
BIGRAG_EMBEDDING_CONCURRENCY=16 # Parallel embedding requests
-BIGRAG_QDRANT_SEARCH_EF=128 # Optional HNSW recall/latency tuning
# Worker command
bigrag-worker --processes 1 --threads 8
@@ -279,7 +270,7 @@ Text logs are colored by default with fixed columns for time, level, logger,
event, and fields. Text rendering escapes control characters in event and field
values before writing to stdout, so request paths and headers cannot create
extra terminal lines or emit terminal control sequences. Dependency loggers such
-as Dramatiq, Alembic, Uvicorn access, HTTPX, and Qdrant are kept at warning level
+as Dramatiq, Alembic, Uvicorn access, HTTPX, and Turbopuffer are kept at warning level
so local terminals show bigRAG actions instead of library startup chatter. Every
API request logs
`request_start`, `response_start`, and `request_complete`; unhandled failures log
@@ -322,7 +313,6 @@ Ensure all infrastructure services are running and healthy:
```bash
docker exec bigrag-postgres pg_isready -U bigrag
-curl -f http://localhost:6333/healthz
docker exec bigrag-redis redis-cli ping
```
diff --git a/website/content/docs/deployment/railway.mdx b/website/content/docs/deployment/railway.mdx
index 0d7e4b6b..8e11fc87 100644
--- a/website/content/docs/deployment/railway.mdx
+++ b/website/content/docs/deployment/railway.mdx
@@ -1,11 +1,11 @@
---
title: Railway
-description: Deploy bigRAG to Railway with managed Postgres and Redis plus self-hosted Qdrant.
+description: Deploy bigRAG to Railway with managed Postgres, Redis, and Turbopuffer search.
---
import { Callout } from "fumadocs-ui/components/callout";
-This guide deploys the API, Dramatiq worker, admin UI, Postgres, Redis, and Qdrant in one Railway project. Qdrant runs as a single Docker-image service and does not need etcd in this setup.
+This guide deploys the API, Dramatiq worker, admin UI, Postgres, and Redis in one Railway project. Turbopuffer provides managed vector, keyword, and hybrid search.
## What you get
@@ -14,11 +14,12 @@ This guide deploys the API, Dramatiq worker, admin UI, Postgres, Redis, and Qdra
- **App** — admin UI, public domain.
- **Postgres 17** — Railway plugin, exposes `DATABASE_URL`.
- **Redis 7** — Railway plugin, exposes `REDIS_URL`.
-- **Qdrant** — Qdrant Docker image, internal-only.
+- **Turbopuffer** — managed vector and full-text search, configured with environment variables.
## Prerequisites
-- Railway Pro plan for comfortable API and vector-store memory limits.
+- Railway Pro plan for comfortable API and worker memory limits.
+- A Turbopuffer API key.
- An OpenAI, Cohere, Voyage, or OpenAI-compatible embedding provider key.
- A region close to your users and embedding provider.
@@ -28,21 +29,9 @@ Provision in this order so API variables can reference private service domains:
1. Postgres plugin.
2. Redis plugin.
-3. `Qdrant` Docker image.
-4. `App` from the repository root.
-5. `API` from the repo root directory `/api`.
-6. `Worker` from the repo root directory `/api`.
-
-## Qdrant
-
-| Setting | Value |
-|---|---|
-| Source | Docker image `qdrant/qdrant:v1.17.1` |
-| Start command | leave empty |
-| Volume | 10 GB at `/qdrant/storage` |
-| Public domain | none |
-
-Qdrant exposes HTTP on `6333` and gRPC on `6334`. The API uses HTTP.
+3. `App` from the repository root.
+4. `API` from the repo root directory `/api`.
+5. `Worker` from the repo root directory `/api`.
## App
@@ -79,7 +68,9 @@ BIGRAG_LOG_LEVEL=info
BIGRAG_LOG_FORMAT=json
BIGRAG_DATABASE_URL=${{Postgres.DATABASE_URL}}
BIGRAG_REDIS_URL=${{Redis.REDIS_URL}}
-BIGRAG_QDRANT_URL=http://${{Qdrant.RAILWAY_PRIVATE_DOMAIN}}:6333
+BIGRAG_TURBOPUFFER_API_KEY=
+BIGRAG_TURBOPUFFER_REGION=aws-us-east-1
+BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX=bigrag_
BIGRAG_EMBEDDING_API_KEY=
BIGRAG_MASTER_KEY=
BIGRAG_ALLOW_PUBLIC_BIND_IN_PROD=true
@@ -89,7 +80,7 @@ BIGRAG_CORS_ORIGINS=["https://${{App.RAILWAY_PUBLIC_DOMAIN}}"]
BIGRAG_UPLOAD_DIR=/data/uploads
```
-Use the Railway Qdrant service URL for `BIGRAG_QDRANT_URL`.
+Use the Turbopuffer region closest to your Railway services and embedding provider.
## Worker
@@ -102,7 +93,7 @@ Use the Railway Qdrant service URL for `BIGRAG_QDRANT_URL`.
| Volume | same mounted upload volume path as API |
| Public domain | no |
-Use the same environment as `API`, including `BIGRAG_DATABASE_URL`, `BIGRAG_REDIS_URL`, `BIGRAG_QDRANT_URL`, `BIGRAG_MASTER_KEY`, and `BIGRAG_UPLOAD_DIR`.
+Use the same environment as `API`, including `BIGRAG_DATABASE_URL`, `BIGRAG_REDIS_URL`, `BIGRAG_TURBOPUFFER_API_KEY`, `BIGRAG_TURBOPUFFER_REGION`, `BIGRAG_MASTER_KEY`, and `BIGRAG_UPLOAD_DIR`.
## Generate the master key
@@ -116,7 +107,7 @@ Back the master key up immediately. It encrypts provider secrets, webhook secret
## First Run
-1. Wait for Postgres, Redis, Qdrant, API, Worker, and app services to deploy.
+1. Wait for Postgres, Redis, API, Worker, and app services to deploy.
2. Open the admin UI public URL.
3. Complete `/setup` to create the first admin account.
4. Create a collection and upload a small document.
@@ -124,7 +115,7 @@ Back the master key up immediately. It encrypts provider secrets, webhook secret
## Troubleshooting
-**API deploy logs stop while connecting to Qdrant.** Keep the default optional Qdrant readiness behavior so the API can boot and `/health` can respond while `/health/ready` reports the degraded vector-store dependency. Fix the Qdrant service URL or credentials, then redeploy.
+**`/health/ready` reports a degraded vector store.** Check `BIGRAG_TURBOPUFFER_API_KEY`, `BIGRAG_TURBOPUFFER_REGION`, and any outbound network restrictions, then redeploy the API and Worker services.
**API exits with `ImportError: Can't find Python file .../site-packages/alembic/env.py`.** Rebuild from the latest repo revision and clear Railway's build cache if it keeps reusing an old image. The API image bundles bigRAG's Alembic migration environment into the installed package; this error means the deployed image was built without that migration environment.
diff --git a/website/content/docs/getting-started/configuration.mdx b/website/content/docs/getting-started/configuration.mdx
index a311b762..e73f630a 100644
--- a/website/content/docs/getting-started/configuration.mdx
+++ b/website/content/docs/getting-started/configuration.mdx
@@ -5,7 +5,7 @@ description: Bootstrap bigRAG, then manage runtime settings from the admin UI.
import { Callout } from "fumadocs-ui/components/callout";
-bigRAG uses a UI-first configuration model. Operators provide only the wiring needed before the API can read Postgres, then admins manage product/runtime settings from `/settings`, `/data-storage`, `/models`, and `/vector-storage`.
+bigRAG uses a UI-first configuration model. Operators provide only the wiring needed before the API can read Postgres, then admins manage product/runtime settings from `/settings`, `/data-storage`, and `/models`.
Auth is admin accounts + session cookies and minted `bigrag_sk_…` API keys — see [Authentication](/docs/api-reference/authentication).
@@ -26,9 +26,10 @@ Bootstrap config is the small set of values the API needs before it can connect
| `BIGRAG_DB_POOL_MAX` | Maximum Postgres pool size. | `50` |
| `BIGRAG_MIGRATION_TIMEOUT_SECONDS` | Startup migration timeout. Set `0` to disable the timeout. | `60` |
| `BIGRAG_REDIS_URL` | Redis connection URL. | `redis://localhost:6379/0` |
-| `BIGRAG_QDRANT_URL` | Qdrant connection URL. | `http://localhost:6333` |
-| `BIGRAG_QDRANT_CONNECT_TIMEOUT_SECONDS` | Qdrant startup connection timeout. | `10` |
-| `BIGRAG_QDRANT_REQUIRED` | Fail startup if configured vector-store clients cannot be reached. | `false` |
+| `BIGRAG_TURBOPUFFER_API_KEY` | Turbopuffer API key for vector search. | — |
+| `BIGRAG_TURBOPUFFER_REGION` | Turbopuffer region. | `aws-us-east-1` |
+| `BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX` | Prefix prepended to generated Turbopuffer namespace names. | `bigrag_` |
+| `BIGRAG_TURBOPUFFER_BASE_URL` | Optional Turbopuffer-compatible endpoint for local/e2e services. | — |
| `BIGRAG_MASTER_KEY` | Fernet key for encrypted provider secrets, instance-setting secrets, embedding-cache rows, and Redis cache payloads. Required for production. | — |
| `BIGRAG_MASTER_KEY_PREVIOUS` | JSON array of old Fernet keys during staged key rotation. | `[]` |
| `BIGRAG_UPLOAD_DIR` | Local document upload directory when storage backend is `local`. | `./data/uploads` |
@@ -55,7 +56,9 @@ workers = 1
database_url = "postgres://bigrag:change-me@postgres:5432/bigrag?sslmode=disable"
redis_url = "redis://redis:6379/0"
-qdrant_url = "http://qdrant:6333"
+turbopuffer_api_key = "tpuf_..."
+turbopuffer_region = "aws-us-east-1"
+turbopuffer_namespace_prefix = "bigrag_"
master_key = "generate-a-fernet-key"
master_key_previous = []
@@ -67,7 +70,7 @@ Environment variables override TOML values. CLI flags passed to `python -m bigra
## Runtime settings
-After the first admin account exists, go to `/settings` to manage platform runtime settings, `/data-storage` to manage uploaded source-file storage, `/models` to manage model presets and model runtime settings, and `/vector-storage` to manage vector backend connection details. New collections choose their vector store during creation. Runtime settings are stored in Postgres in the `instance_settings` table. Secret settings are encrypted with `BIGRAG_MASTER_KEY` and redacted on read.
+After the first admin account exists, go to `/settings` to manage platform runtime settings, `/data-storage` to manage uploaded source-file storage, and `/models` to manage model presets and model runtime settings. Turbopuffer is the vector backend for every collection. Runtime settings are stored in Postgres in the `instance_settings` table. Secret settings are encrypted with `BIGRAG_MASTER_KEY` and redacted on read.
The same surface is available through [`/v1/admin/settings`](/docs/api-reference/instance-settings).
@@ -77,14 +80,14 @@ The same surface is available through [`/v1/admin/settings`](/docs/api-reference
| Data | Upload limits, OCR, ingestion workers, queue depth, webhook delivery limits, and retention windows. |
| Data Storage (`/data-storage`) | Storage backend, S3-compatible bucket, endpoint, region, prefix, credentials, and path-style behavior. |
| Models (`/models`) | Default embedding provider, model, dimension, key, base URL, cache TTLs, chat provider, chat model, temperature, history count, and context budget. |
-| Vector Storage (`/vector-storage`) | Provider tabs for Qdrant URL, readiness behavior, and HNSW tuning or turbopuffer API key, region, and namespace prefix. |
+| Vector search | Turbopuffer API key, region, namespace prefix, and optional compatible base URL. |
| Backups | S3-compatible readable backup bucket, endpoint, region, prefix, and credentials for S3, R2, or MinIO. |
`BIGRAG_CHAT_API_KEY` can seed a default instance chat credential for OpenAI's standard API endpoint. For non-default `chat_base_url` values, configure a saved chat key in the admin UI or pass `provider_api_key` on chat requests so the instance fallback key is never sent to a custom provider.
The UI uses the same compact top tab control and shared dashboard body width as the other admin pages for Account, Health, Security, and Data, then opens directly into the selected Settings area without a repeated section heading. Each area is arranged sequentially from top to bottom. Each runtime panel shows common controls first, keeps raw setting keys, source metadata, and rarely changed limits in Advanced controls, keeps Save in the panel footer, and uses default or example placeholders for empty controls. Settings only accepts current tab values: `/settings?tab=account`, `/settings?tab=health`, `/settings?tab=security`, and `/settings?tab=data`; file storage lives at `/data-storage`.
-Database-backed admin settings apply immediately on save. Storage, backup, and vector-store connection changes validate the new target before saving, then swap the runtime client in place where applicable.
+Database-backed admin settings apply immediately on save. Storage, backup, and Turbopuffer connection changes validate the new target before saving, then swap the runtime client in place where applicable.
Legacy runtime environment variables are still read as initial defaults for compatibility, but enterprise installs should treat `/settings` as the control plane after bootstrap.
@@ -99,6 +102,5 @@ Legacy runtime environment variables are still read as initial defaults for comp
--host HOST Override server host
--port PORT Override server port
--database-url URL Override Postgres URL
---qdrant-url URL Override Qdrant URL
--redis-url URL Override Redis URL
```
diff --git a/website/content/docs/getting-started/installation.mdx b/website/content/docs/getting-started/installation.mdx
index bda4a77d..c5d6a3f4 100644
--- a/website/content/docs/getting-started/installation.mdx
+++ b/website/content/docs/getting-started/installation.mdx
@@ -12,6 +12,7 @@ import { Callout } from "fumadocs-ui/components/callout";
- **Docker** and **Docker Compose** (recommended)
- **Python 3.12+** and [**uv**](https://docs.astral.sh/uv/) (for running the backend from source)
- An **OpenAI** or **Cohere** API key for embeddings
+- A **Turbopuffer** API key for vector search
## Install
@@ -27,7 +28,6 @@ docker compose up -d
This starts:
- **bigRAG API** on port `4000` (Swagger docs at `/docs`)
-- **Qdrant** vector database on port `6333`
- **PostgreSQL** for metadata on port `5432`
- **Redis** for the ingestion queue on port `6379`
@@ -48,7 +48,7 @@ Release artifacts use CalVer (`YYYY.M.D`). Docker also publishes `latest` for co
### Start infrastructure services
```bash
-docker compose up postgres redis qdrant -d
+docker compose up postgres redis -d
```
@@ -86,7 +86,7 @@ This script:
1. Kills stale processes on port 4000
2. Validates required commands (`docker`, `curl`, `uv`)
-3. Starts Docker services (Postgres, Redis, Qdrant) and waits for readiness
+3. Starts Docker services (Postgres and Redis) and waits for readiness
4. Installs Python dependencies with `uv`
5. Starts the backend with auto-reload
6. Starts `bigrag-worker` with Dramatiq on Redis
diff --git a/website/content/docs/index.mdx b/website/content/docs/index.mdx
index 4a31c766..e75a810d 100644
--- a/website/content/docs/index.mdx
+++ b/website/content/docs/index.mdx
@@ -15,11 +15,11 @@ bigRAG is an open-source RAG (Retrieval-Augmented Generation) platform. It provi
- **Any document format** — PDF, DOCX, PPTX, XLSX, HTML, Markdown, CSV/TSV/XML/JSON, and scanned images (OCR) via [Docling](https://github.com/DS4SD/docling)
- **Any embedding model** — OpenAI, Cohere, Voyage, or `openai_compatible` gateways (Ollama, vLLM, TEI, LiteLLM, Azure, Bedrock) configured per collection
- **Three search modes** — semantic, keyword, and hybrid, with optional Cohere reranking
-- **Qdrant vector database** — HNSW vector search with tenant-aware payload indexes
+- **Turbopuffer vector search** — semantic, keyword, and hybrid retrieval with namespace isolation
- **First-class auth** — admin accounts, session cookies, scoped `bigrag_sk_…` API keys, and full audit/access logs
- **Operational features** — batch ingestion, Google Drive sync, webhooks with HMAC signatures, metadata-schema validation, an evaluation runner, and per-collection query analytics
- **Admin UI** — first-run setup, collection browser, stateless chat, connector management, access logs, MCP credentials, and API-key minting
-- **Self-hostable** — Docker Compose, no external dependencies
+- **Self-hostable control plane** — Docker Compose for the API, admin UI, Postgres, and Redis
- **MIT licensed** — run it anywhere, forever free
## Architecture
@@ -37,10 +37,10 @@ bigRAG is an open-source RAG (Retrieval-Augmented Generation) platform. It provi
Redis --> Worker["Ingestion worker"]
Worker --> Docling["Docling conversion"]
Docling --> Embeddings["Embedding providers"]
- Embeddings --> Qdrant[("Qdrant vectors")]
+ Embeddings --> Turbopuffer[("Turbopuffer vectors")]
Retrieval --> Embeddings
- Retrieval --> Qdrant
+ Retrieval --> Turbopuffer
Retrieval --> LLM["Chat provider"]
LLM --> Answer["Retrieved answer"]
@@ -55,7 +55,7 @@ bigRAG is an open-source RAG (Retrieval-Augmented Generation) platform. It provi
class API api
class Control,Ingestion,Retrieval plane
class Redis,Worker,Docling pipeline
- class Postgres,Files,Embeddings,Qdrant,LLM store
+ class Postgres,Files,Embeddings,Turbopuffer,LLM store
class Answer output' />
| Component | Purpose | Default Address |
@@ -63,7 +63,7 @@ bigRAG is an open-source RAG (Retrieval-Augmented Generation) platform. It provi
| **bigRAG API** | REST API server (FastAPI) | `http://localhost:4000` |
| **Admin UI** | Admin web app | `http://localhost:3000` |
| **PostgreSQL** | Users, sessions, API keys, metadata, audit | `localhost:5432` |
-| **Qdrant** | Vector storage and search | `localhost:6333` |
+| **Turbopuffer** | Vector storage and semantic, keyword, and hybrid search | Managed service |
| **Redis** | Ingestion + event bus | `localhost:6379` |
## Explore
diff --git a/website/content/docs/migration/from-pinecone.mdx b/website/content/docs/migration/from-pinecone.mdx
index 6fc971d1..f450c3f0 100644
--- a/website/content/docs/migration/from-pinecone.mdx
+++ b/website/content/docs/migration/from-pinecone.mdx
@@ -90,15 +90,15 @@ Recommended if you're in the middle of a chunking rethink anyway.
## API differences to watch
- **Filters stay as JSON objects**. Pass a dict like
- `{"department": "sales"}` and bigRAG translates it into Qdrant
- payload filters.
+ `{"department": "sales"}` and bigRAG translates it into Turbopuffer
+ filters.
- **Namespaces → metadata filters**. Pinecone's `namespace="docs"`
becomes `metadata.namespace = "docs"`. Set
`tenant_field: "namespace"` on the collection when that field should
- get a Qdrant payload index and become required on uploads, raw vector
- upserts, queries, and chat requests.
+ become required on uploads, raw vector upserts, queries, and chat
+ requests.
- **Hybrid search is built-in**. `search_mode: "hybrid"` runs keyword
- + vector in parallel and fuses with reciprocal rank fusion.
+ BM25 + vector search in parallel and fuses with reciprocal rank fusion.
No separate "sparse" vectors to maintain.
## Cut-over
diff --git a/website/content/docs/sdks/python.mdx b/website/content/docs/sdks/python.mdx
index e59ed9a8..ebee5fa6 100644
--- a/website/content/docs/sdks/python.mdx
+++ b/website/content/docs/sdks/python.mdx
@@ -72,7 +72,7 @@ The SDK follows a resource namespace pattern:
| `client.vectors` | Raw vector upsert and delete |
| `client.webhooks` | Webhook management |
| `client.auth` | Setup, login, logout, identity, password, and preferences |
-| `client.admin` | Users, API keys, access logs, audit logs, runtime settings, backups, vector migrations, admin realtime streams, connector config, embedding presets, and MCP server keys |
+| `client.admin` | Users, API keys, access logs, audit logs, runtime settings, backups, admin realtime streams, connector config, embedding presets, and MCP server keys |
| `client.connectors.google` | Google Drive account, file browsing, sources, and sync jobs |
| `client.evaluations` | Golden-set retrieval evaluation runs |
@@ -87,7 +87,6 @@ result = await client.collections.list(name="prefix", limit=10)
# Create
collection = await client.collections.create({
"name": "docs",
- "vector_store_provider": "qdrant",
"embedding_provider": "openai",
"embedding_model": "text-embedding-3-small",
})
@@ -328,34 +327,19 @@ except APITimeoutError:
```python
settings = await client.admin.settings.list()
-await client.admin.settings.test({"values": {"qdrant_url": "http://qdrant:6333"}})
+await client.admin.settings.test({"values": {"turbopuffer_region": "aws-us-west-2"}})
await client.admin.settings.update({"values": {"trusted_proxies": ["10.0.0.0/8"]}})
await client.admin.settings.reset({"keys": ["trusted_proxies"]})
await client.admin.settings.purge_embedding_cache()
backups = await client.admin.backups.list(limit=20)
-backup = await client.admin.backups.create({"label": "before migration"})
+backup = await client.admin.backups.create({"label": "nightly export"})
backup = await client.admin.backups.get(backup["id"])
-vector_migrations = await client.admin.vector_migrations.list(
- collection="docs",
- include_total=True,
-)
-vector_migration = await client.admin.vector_migrations.create({
- "collection": "docs",
- "target_provider": "turbopuffer",
-})
-vector_migration = await client.admin.vector_migrations.get(vector_migration["id"])
-await client.admin.vector_migrations.delete(vector_migration["id"])
-
async for event in client.admin.realtime.backups(limit=20):
if event["event"] == "snapshot":
print(event["data"]["payload"]["jobs"])
-async for event in client.admin.realtime.vector_migrations(collection="docs"):
- if event["event"] == "snapshot":
- print(event["data"]["payload"]["jobs"])
-
async for event in client.admin.realtime.platform_readiness():
print(event["data"])
```
@@ -366,7 +350,7 @@ async for event in client.admin.realtime.platform_readiness():
# Health check
health = await client.health()
-# Readiness (checks Postgres, Qdrant, Redis, embedding provider)
+# Readiness (checks Postgres, Redis, Turbopuffer, embedding provider)
ready = await client.readiness()
# Platform stats
diff --git a/website/content/docs/sdks/typescript.mdx b/website/content/docs/sdks/typescript.mdx
index 8635e64a..570b93b5 100644
--- a/website/content/docs/sdks/typescript.mdx
+++ b/website/content/docs/sdks/typescript.mdx
@@ -70,7 +70,6 @@ client.collections.listAll({ name?, limit? }) // AsyncGenerator that pages thro
client.collections.create({
name,
description?,
- vector_store_provider?, // "qdrant" | "turbopuffer"
embedding_preset_id?,
embedding_provider?, // "openai" | "cohere" | "voyage" | "openai_compatible"
embedding_model?,
@@ -78,7 +77,6 @@ client.collections.create({
embedding_base_url?,
dimension?,
chunk_size?, chunk_overlap?, chunk_strategy?, // "paragraph" | "recursive"
- index_type?, // "HNSW"
tenant_field?, // metadata key required for tenant filtering
metadata_schema?, // JSON Schema
reranking_enabled?, reranking_model?, reranking_api_key?,
@@ -238,11 +236,6 @@ client.admin.backups.list({ limit?, offset? })
client.admin.backups.get(backupId)
client.admin.backups.create({ label? })
-client.admin.vectorMigrations.list({ collection?, cursor?, includeTotal?, limit?, offset? })
-client.admin.vectorMigrations.get(migrationId)
-client.admin.vectorMigrations.create({ collection, target_provider })
-client.admin.vectorMigrations.delete(migrationId)
-
client.admin.connectors.google.get()
client.admin.connectors.google.update({ enabled?, client_id?, client_secret? })
@@ -267,12 +260,6 @@ for await (const event of client.admin.realtime.backups({ limit: 20 })) {
}
}
-for await (const event of client.admin.realtime.vectorMigrations({ collection: "docs" })) {
- if (event.event === "snapshot") {
- console.log(event.data.payload.jobs)
- }
-}
-
for await (const event of client.admin.realtime.platformReadiness()) {
console.log(event.data)
}
From f8ff0e26ed8513a517dcb237d063778472e89ff2 Mon Sep 17 00:00:00 2001
From: Bigint <69431456+bigint@users.noreply.github.com>
Date: Thu, 21 May 2026 23:12:18 +0530
Subject: [PATCH 5/9] chore: finalize turbopuffer-only cleanup
---
api/bigrag/services/collection_provision.py | 4 +-
e2e/stubs/fake_openai/app/main.py | 8 +---
e2e/tests/api/test_access_logs.py | 36 +++++-------------
e2e/tests/api/test_api_keys.py | 4 +-
e2e/tests/api/test_audit.py | 20 +++-------
e2e/tests/api/test_auth.py | 8 +---
e2e/tests/api/test_backups.py | 16 ++++----
e2e/tests/api/test_batch.py | 1 -
e2e/tests/api/test_documents.py | 42 ++++++---------------
e2e/tests/api/test_documents_progress.py | 5 +--
e2e/tests/api/test_embedding_presets.py | 40 ++++++--------------
e2e/tests/api/test_evaluation.py | 4 +-
e2e/tests/api/test_mcp_servers.py | 8 +---
e2e/tests/api/test_query.py | 12 ++----
e2e/tests/api/test_sse_events.py | 18 +++------
e2e/tests/api/test_upload_sessions.py | 16 ++------
e2e/tests/api/test_users.py | 4 +-
e2e/tests/api/test_webhooks.py | 8 +---
e2e/tests/sdk_python/conftest.py | 5 +--
e2e/tests/sdk_python/test_auth.py | 2 +-
e2e/tests/sdk_python/test_chat_stream.py | 9 +----
e2e/tests/sdk_python/test_documents.py | 6 +--
e2e/tests/sdk_python/test_errors.py | 10 ++---
e2e/tests/sdk_python/test_query.py | 14 ++-----
sdks/python/src/bigrag/types/collections.py | 2 -
sdks/typescript/src/types/collections.ts | 2 -
website/content/docs/admin-ui.mdx | 17 ++++-----
27 files changed, 90 insertions(+), 231 deletions(-)
diff --git a/api/bigrag/services/collection_provision.py b/api/bigrag/services/collection_provision.py
index c34f6f9e..8c23088e 100644
--- a/api/bigrag/services/collection_provision.py
+++ b/api/bigrag/services/collection_provision.py
@@ -68,8 +68,8 @@ async def create_vector_store_collection(
if "API key is not configured" in message or "client is not connected" in message:
raise HTTPException(
status_code=400,
- detail=vector_store_unavailable_detail(),
- ) from e
+ detail=vector_store_unavailable_detail(),
+ ) from e
logger.warning(
"vector collection create failed",
collection=body.name,
diff --git a/e2e/stubs/fake_openai/app/main.py b/e2e/stubs/fake_openai/app/main.py
index e67e9729..5843f87c 100644
--- a/e2e/stubs/fake_openai/app/main.py
+++ b/e2e/stubs/fake_openai/app/main.py
@@ -128,9 +128,7 @@ async def event_stream():
"object": "chat.completion.chunk",
"created": created,
"model": model,
- "choices": [
- {"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}
- ],
+ "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
}
yield _sse_chunk(first)
@@ -140,9 +138,7 @@ async def event_stream():
"object": "chat.completion.chunk",
"created": created,
"model": model,
- "choices": [
- {"index": 0, "delta": {"content": piece}, "finish_reason": None}
- ],
+ "choices": [{"index": 0, "delta": {"content": piece}, "finish_reason": None}],
}
yield _sse_chunk(chunk)
diff --git a/e2e/tests/api/test_access_logs.py b/e2e/tests/api/test_access_logs.py
index 5eb4614f..81855653 100644
--- a/e2e/tests/api/test_access_logs.py
+++ b/e2e/tests/api/test_access_logs.py
@@ -33,9 +33,7 @@ async def _wait_for_action(
timeout: float = 10.0,
) -> list[dict]:
async def _fetch() -> list[dict]:
- r = await admin_client.get(
- "/v1/admin/access/logs", params={"action": action, "limit": 50}
- )
+ r = await admin_client.get("/v1/admin/access/logs", params={"action": action, "limit": 50})
r.raise_for_status()
return r.json().get("entries", [])
@@ -175,9 +173,7 @@ async def test_logs_filter_by_method(
await _seed_query(admin_client, coll["name"])
await _wait_for_action(admin_client, "query.run")
- resp = await admin_client.get(
- "/v1/admin/access/logs", params={"method": "post", "limit": 20}
- )
+ resp = await admin_client.get("/v1/admin/access/logs", params={"method": "post", "limit": 20})
body = assert_envelope(resp, 200)
for entry in body["entries"]:
assert entry["method"] == "POST"
@@ -193,9 +189,7 @@ async def test_logs_filter_by_path_substring(
await _seed_query(admin_client, coll["name"])
await _wait_for_action(admin_client, "query.run")
- resp = await admin_client.get(
- "/v1/admin/access/logs", params={"path": "/query", "limit": 20}
- )
+ resp = await admin_client.get("/v1/admin/access/logs", params={"path": "/query", "limit": 20})
body = assert_envelope(resp, 200)
for entry in body["entries"]:
assert "/query" in entry["path"]
@@ -229,9 +223,7 @@ async def test_logs_filter_by_success(
await _seed_query(admin_client, coll["name"])
await _wait_for_action(admin_client, "query.run")
- resp = await admin_client.get(
- "/v1/admin/access/logs", params={"success": "true", "limit": 20}
- )
+ resp = await admin_client.get("/v1/admin/access/logs", params={"success": "true", "limit": 20})
body = assert_envelope(resp, 200)
for entry in body["entries"]:
assert entry["success"] is True
@@ -240,18 +232,14 @@ async def test_logs_filter_by_success(
async def test_logs_invalid_actor_id_returns_400(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.get(
- "/v1/admin/access/logs", params={"actor_id": "not-a-uuid"}
- )
+ resp = await admin_client.get("/v1/admin/access/logs", params={"actor_id": "not-a-uuid"})
assert resp.status_code == 400
async def test_logs_invalid_status_family_returns_422(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.get(
- "/v1/admin/access/logs", params={"status_family": "9xx"}
- )
+ resp = await admin_client.get("/v1/admin/access/logs", params={"status_family": "9xx"})
assert resp.status_code == 422
@@ -298,9 +286,7 @@ async def test_overview_shape_and_window(
await _seed_query(admin_client, coll["name"])
await _wait_for_action(admin_client, "query.run")
- resp = await admin_client.get(
- "/v1/admin/access/overview", params={"window_days": 7}
- )
+ resp = await admin_client.get("/v1/admin/access/overview", params={"window_days": 7})
body = assert_envelope(resp, 200)
for key in (
"window_days",
@@ -332,12 +318,8 @@ async def test_overview_default_window(admin_client: httpx.AsyncClient) -> None:
async def test_overview_invalid_window_rejected(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.get(
- "/v1/admin/access/overview", params={"window_days": 0}
- )
+ resp = await admin_client.get("/v1/admin/access/overview", params={"window_days": 0})
assert resp.status_code == 422
- resp = await admin_client.get(
- "/v1/admin/access/overview", params={"window_days": 1000}
- )
+ resp = await admin_client.get("/v1/admin/access/overview", params={"window_days": 1000})
assert resp.status_code == 422
diff --git a/e2e/tests/api/test_api_keys.py b/e2e/tests/api/test_api_keys.py
index cb3e1d4b..1e4347f4 100644
--- a/e2e/tests/api/test_api_keys.py
+++ b/e2e/tests/api/test_api_keys.py
@@ -197,7 +197,5 @@ async def test_patch_api_key_invalid_id_returns_404(
async def test_delete_api_key_unknown_id_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.delete(
- "/v1/admin/api-keys/00000000-0000-0000-0000-000000000000"
- )
+ resp = await admin_client.delete("/v1/admin/api-keys/00000000-0000-0000-0000-000000000000")
assert resp.status_code == 404, resp.text
diff --git a/e2e/tests/api/test_audit.py b/e2e/tests/api/test_audit.py
index 46b629f8..2e34c0ce 100644
--- a/e2e/tests/api/test_audit.py
+++ b/e2e/tests/api/test_audit.py
@@ -100,9 +100,7 @@ async def test_audit_filter_by_actor_id(
async def test_audit_filter_invalid_actor_id(admin_client: httpx.AsyncClient) -> None:
- resp = await admin_client.get(
- "/v1/admin/audit", params={"actor_id": "not-a-uuid"}
- )
+ resp = await admin_client.get("/v1/admin/audit", params={"actor_id": "not-a-uuid"})
assert resp.status_code == 400
@@ -113,16 +111,12 @@ async def test_audit_pagination(
for _ in range(3):
await api_key(name=unique_name("audit"))
- resp_first = await admin_client.get(
- "/v1/admin/audit", params={"limit": 2, "offset": 0}
- )
+ resp_first = await admin_client.get("/v1/admin/audit", params={"limit": 2, "offset": 0})
first_page = assert_envelope(resp_first, 200)
assert len(first_page["entries"]) <= 2
assert "total" in first_page
- resp_second = await admin_client.get(
- "/v1/admin/audit", params={"limit": 2, "offset": 2}
- )
+ resp_second = await admin_client.get("/v1/admin/audit", params={"limit": 2, "offset": 2})
second_page = assert_envelope(resp_second, 200)
first_ids = {e["id"] for e in first_page["entries"]}
second_ids = {e["id"] for e in second_page["entries"]}
@@ -173,9 +167,7 @@ async def _fetch(action: str) -> dict:
# audit.record uses safe_create_task → polling absorbs the race.
await poll_until(
lambda: _fetch("webhook.create"),
- predicate=lambda b: any(
- e["resource_id"] == created["id"] for e in b["entries"]
- ),
+ predicate=lambda b: any(e["resource_id"] == created["id"] for e in b["entries"]),
timeout=10.0,
interval=0.3,
description=f"audit webhook.create entry for {created['id']}",
@@ -185,9 +177,7 @@ async def _fetch(action: str) -> dict:
await poll_until(
lambda: _fetch("webhook.delete"),
- predicate=lambda b: any(
- e["resource_id"] == created["id"] for e in b["entries"]
- ),
+ predicate=lambda b: any(e["resource_id"] == created["id"] for e in b["entries"]),
timeout=10.0,
interval=0.3,
description=f"audit webhook.delete entry for {created['id']}",
diff --git a/e2e/tests/api/test_auth.py b/e2e/tests/api/test_auth.py
index 7a399fd4..42846f8e 100644
--- a/e2e/tests/api/test_auth.py
+++ b/e2e/tests/api/test_auth.py
@@ -24,9 +24,7 @@
API_BASE_HEADERS = {"Origin": "http://localhost:4000"}
-async def _login(
- client: httpx.AsyncClient, email: str, password: str
-) -> httpx.Response:
+async def _login(client: httpx.AsyncClient, email: str, password: str) -> httpx.Response:
return await client.post(
"/v1/auth/login",
json={"email": email, "password": password},
@@ -114,9 +112,7 @@ async def test_logout_all_invalidates_other_sessions(
admin_client: httpx.AsyncClient,
unauth_client: httpx.AsyncClient,
) -> None:
- second_login = await _login(
- unauth_client, admin_setup["email"], admin_setup["password"]
- )
+ second_login = await _login(unauth_client, admin_setup["email"], admin_setup["password"])
assert second_login.status_code == 200, second_login.text
other_me = await unauth_client.get("/v1/auth/me")
assert other_me.status_code == 200
diff --git a/e2e/tests/api/test_backups.py b/e2e/tests/api/test_backups.py
index 31ab31ca..0f7b9f43 100644
--- a/e2e/tests/api/test_backups.py
+++ b/e2e/tests/api/test_backups.py
@@ -42,7 +42,10 @@ def ensure_backup_bucket() -> None:
try:
import boto3 # type: ignore[import-untyped]
from botocore.client import Config # type: ignore[import-untyped]
- from botocore.exceptions import ClientError, EndpointConnectionError # type: ignore[import-untyped]
+ from botocore.exceptions import ( # type: ignore[import-untyped]
+ ClientError,
+ EndpointConnectionError,
+ )
except ImportError as exc:
pytest.skip(f"boto3 not available: {exc}")
@@ -119,9 +122,7 @@ async def test_list_backups_returns_shape(admin_client: httpx.AsyncClient) -> No
async def test_get_backup_unknown_id_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.get(
- "/v1/admin/backups/00000000-0000-0000-0000-000000000000"
- )
+ resp = await admin_client.get("/v1/admin/backups/00000000-0000-0000-0000-000000000000")
assert resp.status_code == 404, resp.text
@@ -174,17 +175,14 @@ async def _fetch() -> dict:
)
if final["status"] == "failed":
pytest.skip(
- "backup job failed (likely environment-specific); "
- f"error={final.get('error_message')!r}"
+ f"backup job failed (likely environment-specific); error={final.get('error_message')!r}"
)
assert final["progress"] == 1.0
assert final["object_count"] >= 1
assert final["destination_prefix"]
# And it must show up in the list.
- list_resp = await admin_client.get(
- "/v1/admin/backups", params={"limit": 50}
- )
+ list_resp = await admin_client.get("/v1/admin/backups", params={"limit": 50})
list_body = assert_envelope(list_resp, 200)
assert any(j["id"] == job_id for j in list_body["jobs"])
diff --git a/e2e/tests/api/test_batch.py b/e2e/tests/api/test_batch.py
index f2e8c38d..22382c72 100644
--- a/e2e/tests/api/test_batch.py
+++ b/e2e/tests/api/test_batch.py
@@ -28,7 +28,6 @@
from tests._helpers import assert_envelope, read_fixture
-
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
diff --git a/e2e/tests/api/test_documents.py b/e2e/tests/api/test_documents.py
index e80ffa09..2c68ad4e 100644
--- a/e2e/tests/api/test_documents.py
+++ b/e2e/tests/api/test_documents.py
@@ -31,7 +31,6 @@
from tests._helpers import assert_envelope, read_fixture, unique_name
-
# ---------------------------------------------------------------------------
# Upload: happy path across formats
# ---------------------------------------------------------------------------
@@ -138,9 +137,7 @@ async def test_upload_broken_pdf_ends_failed_with_message(
from tests._helpers import poll_until
async def fetch():
- r = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/{body['id']}"
- )
+ r = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{body['id']}")
r.raise_for_status()
return r.json()
@@ -315,9 +312,7 @@ async def test_get_document_returns_full_payload(
) -> None:
coll = await collection()
doc = await document(coll["name"], fixture="sample.txt")
- resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/{doc['id']}"
- )
+ resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}")
body = assert_envelope(resp, 200)
assert body["id"] == doc["id"]
assert body["filename"] == doc["filename"]
@@ -340,9 +335,7 @@ async def test_get_document_invalid_uuid_returns_404(
collection: Callable[..., Awaitable[dict[str, Any]]],
) -> None:
coll = await collection()
- resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/not-a-uuid"
- )
+ resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/not-a-uuid")
assert resp.status_code == 404, resp.text
@@ -353,9 +346,7 @@ async def test_get_document_chunks_shape(
) -> None:
coll = await collection()
doc = await document(coll["name"], fixture="sample.txt")
- resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/{doc['id']}/chunks"
- )
+ resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}/chunks")
body = assert_envelope(resp, 200)
assert "chunks" in body
assert "total" in body
@@ -385,9 +376,7 @@ async def test_download_document_file_matches_content_type(
) -> None:
coll = await collection()
doc = await document(coll["name"], fixture="sample.txt")
- resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/{doc['id']}/file"
- )
+ resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}/file")
assert resp.status_code == 200, resp.text
assert resp.headers["content-type"].startswith("text/plain")
assert b"Acme" in resp.content
@@ -399,8 +388,7 @@ async def test_download_unknown_document_returns_404(
) -> None:
coll = await collection()
resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/"
- "00000000-0000-0000-0000-000000000000/file"
+ f"/v1/collections/{coll['name']}/documents/00000000-0000-0000-0000-000000000000/file"
)
assert resp.status_code == 404, resp.text
@@ -430,8 +418,7 @@ async def test_reprocess_unknown_document_returns_404(
) -> None:
coll = await collection()
resp = await admin_client.post(
- f"/v1/collections/{coll['name']}/documents/"
- "00000000-0000-0000-0000-000000000000/reprocess"
+ f"/v1/collections/{coll['name']}/documents/00000000-0000-0000-0000-000000000000/reprocess"
)
assert resp.status_code == 404, resp.text
@@ -443,14 +430,10 @@ async def test_delete_document_removes_it(
) -> None:
coll = await collection()
doc = await document(coll["name"], fixture="sample.txt")
- resp = await admin_client.delete(
- f"/v1/collections/{coll['name']}/documents/{doc['id']}"
- )
+ resp = await admin_client.delete(f"/v1/collections/{coll['name']}/documents/{doc['id']}")
assert resp.status_code == 200, resp.text
- follow_up = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/{doc['id']}"
- )
+ follow_up = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}")
assert follow_up.status_code == 404, follow_up.text
@@ -460,8 +443,7 @@ async def test_delete_unknown_document_returns_404(
) -> None:
coll = await collection()
resp = await admin_client.delete(
- f"/v1/collections/{coll['name']}/documents/"
- "00000000-0000-0000-0000-000000000000"
+ f"/v1/collections/{coll['name']}/documents/00000000-0000-0000-0000-000000000000"
)
assert resp.status_code == 404, resp.text
@@ -486,9 +468,7 @@ async def test_get_document_cross_collection(
async def test_get_document_cross_collection_unknown_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.get(
- "/v1/documents/00000000-0000-0000-0000-000000000000"
- )
+ resp = await admin_client.get("/v1/documents/00000000-0000-0000-0000-000000000000")
assert resp.status_code == 404, resp.text
diff --git a/e2e/tests/api/test_documents_progress.py b/e2e/tests/api/test_documents_progress.py
index 8d772e2a..3389151f 100644
--- a/e2e/tests/api/test_documents_progress.py
+++ b/e2e/tests/api/test_documents_progress.py
@@ -39,7 +39,6 @@
from tests._helpers import assert_envelope
-
PROGRESS_KEYS = {
"document_id",
"collection_name",
@@ -87,9 +86,7 @@ async def test_get_document_includes_progress_field(
) -> None:
coll = await collection()
doc = await document(coll["name"], fixture="sample.txt")
- resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/documents/{doc['id']}"
- )
+ resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}")
body = assert_envelope(resp, 200)
_assert_progress_shape(body["progress"], doc_id=doc["id"])
diff --git a/e2e/tests/api/test_embedding_presets.py b/e2e/tests/api/test_embedding_presets.py
index cdef372f..a105b64b 100644
--- a/e2e/tests/api/test_embedding_presets.py
+++ b/e2e/tests/api/test_embedding_presets.py
@@ -19,7 +19,6 @@
from tests._helpers import assert_envelope, unique_name
-
PresetFactory = Callable[..., Awaitable[dict[str, Any]]]
@@ -103,18 +102,14 @@ async def test_list_presets_pagination(
) -> None:
a = await preset()
b = await preset()
- resp = await admin_client.get(
- "/v1/admin/embedding-presets", params={"limit": 1, "offset": 0}
- )
+ resp = await admin_client.get("/v1/admin/embedding-presets", params={"limit": 1, "offset": 0})
body = assert_envelope(resp, 200)
assert isinstance(body["presets"], list)
assert len(body["presets"]) == 1
assert body["total"] >= 2
all_ids = {a["id"], b["id"]}
page1_id = body["presets"][0]["id"]
- resp2 = await admin_client.get(
- "/v1/admin/embedding-presets", params={"limit": 1, "offset": 1}
- )
+ resp2 = await admin_client.get("/v1/admin/embedding-presets", params={"limit": 1, "offset": 1})
body2 = assert_envelope(resp2, 200)
assert len(body2["presets"]) == 1
page2_id = body2["presets"][0]["id"]
@@ -143,9 +138,7 @@ async def test_test_saved_preset_against_fake_openai(
preset: PresetFactory,
) -> None:
created = await preset()
- resp = await admin_client.post(
- f"/v1/admin/embedding-presets/{created['id']}/test"
- )
+ resp = await admin_client.post(f"/v1/admin/embedding-presets/{created['id']}/test")
body = assert_envelope(resp, 200)
assert body["status"] == "ok"
@@ -153,9 +146,7 @@ async def test_test_saved_preset_against_fake_openai(
async def test_test_saved_preset_unknown_id_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.post(
- "/v1/admin/embedding-presets/not-a-uuid/test"
- )
+ resp = await admin_client.post("/v1/admin/embedding-presets/not-a-uuid/test")
assert resp.status_code == 404, resp.text
resp2 = await admin_client.post(
@@ -242,9 +233,10 @@ async def test_collection_can_use_preset(
) -> None:
created = await preset()
coll = await collection(embedding_preset_id=created["id"])
- assert coll.get("embedding_preset_id") == created["id"] or coll.get("embedding_preset") is not None or coll.get("embedding_provider") in (
- "openai_compatible",
- "openai",
+ assert (
+ coll.get("embedding_preset_id") == created["id"]
+ or coll.get("embedding_preset") is not None
+ or coll.get("embedding_provider") in ("openai_compatible", "openai")
)
@@ -253,16 +245,12 @@ async def test_delete_preset_makes_it_unfindable(
preset: PresetFactory,
) -> None:
created = await preset()
- resp = await admin_client.delete(
- f"/v1/admin/embedding-presets/{created['id']}"
- )
+ resp = await admin_client.delete(f"/v1/admin/embedding-presets/{created['id']}")
body = assert_envelope(resp, 200)
assert body["status"] == "ok"
# Subsequent lookups should 404.
- test_resp = await admin_client.post(
- f"/v1/admin/embedding-presets/{created['id']}/test"
- )
+ test_resp = await admin_client.post(f"/v1/admin/embedding-presets/{created['id']}/test")
assert test_resp.status_code == 404, test_resp.text
patch_resp = await admin_client.patch(
@@ -284,9 +272,7 @@ async def test_delete_preset_in_use_returns_409(
created = await preset()
coll = await collection(embedding_preset_id=created["id"])
try:
- resp = await admin_client.delete(
- f"/v1/admin/embedding-presets/{created['id']}"
- )
+ resp = await admin_client.delete(f"/v1/admin/embedding-presets/{created['id']}")
assert resp.status_code == 409, resp.text
finally:
# collection factory tears the collection down; force-delete it
@@ -297,9 +283,7 @@ async def test_delete_preset_in_use_returns_409(
async def test_delete_preset_unknown_id_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.delete(
- "/v1/admin/embedding-presets/not-a-uuid"
- )
+ resp = await admin_client.delete("/v1/admin/embedding-presets/not-a-uuid")
assert resp.status_code == 404, resp.text
resp2 = await admin_client.delete(
diff --git a/e2e/tests/api/test_evaluation.py b/e2e/tests/api/test_evaluation.py
index 550b94ac..9a33685f 100644
--- a/e2e/tests/api/test_evaluation.py
+++ b/e2e/tests/api/test_evaluation.py
@@ -28,9 +28,7 @@ def _load_golden_set() -> dict[str, Any]:
return json.loads((GOLDEN_SETS_DIR / "acme.json").read_text())
-def _substitute_ids(
- golden: dict[str, Any], id_map: dict[str, str]
-) -> list[dict[str, Any]]:
+def _substitute_ids(golden: dict[str, Any], id_map: dict[str, str]) -> list[dict[str, Any]]:
cases: list[dict[str, Any]] = []
for entry in golden["queries"]:
expected = [id_map.get(raw, raw) for raw in entry["expected_doc_ids"]]
diff --git a/e2e/tests/api/test_mcp_servers.py b/e2e/tests/api/test_mcp_servers.py
index 1469b7ca..cbc5d9ca 100644
--- a/e2e/tests/api/test_mcp_servers.py
+++ b/e2e/tests/api/test_mcp_servers.py
@@ -282,9 +282,7 @@ async def test_patch_duplicate_server_name_409(
async def test_rotate_returns_new_key(admin_client: httpx.AsyncClient) -> None:
created = await _create(admin_client)
try:
- resp = await admin_client.post(
- f"/v1/admin/mcp-servers/{created['id']}/rotate"
- )
+ resp = await admin_client.post(f"/v1/admin/mcp-servers/{created['id']}/rotate")
body = assert_envelope(resp, 200)
assert body["api_key"].startswith("bigrag_sk_")
assert body["api_key"] != created["api_key"]
@@ -309,9 +307,7 @@ async def test_rotated_old_key_no_longer_authenticates(
resp = await old_client.get("/v1/collections")
assert resp.status_code == 200
- rotated = (
- await admin_client.post(f"/v1/admin/mcp-servers/{created['id']}/rotate")
- ).json()
+ rotated = (await admin_client.post(f"/v1/admin/mcp-servers/{created['id']}/rotate")).json()
async with httpx.AsyncClient(
base_url=str(admin_client.base_url),
diff --git a/e2e/tests/api/test_query.py b/e2e/tests/api/test_query.py
index ee6a2863..4ac751aa 100644
--- a/e2e/tests/api/test_query.py
+++ b/e2e/tests/api/test_query.py
@@ -36,9 +36,7 @@ async def test_query_collection_returns_results_with_timings(
document: DocumentFactory,
) -> None:
coll = await seed_collection(collection, document)
- await wait_until_searchable(
- admin_client, coll["name"], "Acme Corp founded Singapore", top_k=3
- )
+ await wait_until_searchable(admin_client, coll["name"], "Acme Corp founded Singapore", top_k=3)
resp = await admin_client.post(
f"/v1/collections/{coll['name']}/query",
json={"query": "Acme Corp founded Singapore", "top_k": 5},
@@ -176,15 +174,11 @@ async def test_query_cache_hit_on_second_call(
await wait_until_searchable(admin_client, coll["name"], "cache probe Acme")
payload = {"query": "cache probe Acme", "top_k": 5}
- first = await admin_client.post(
- f"/v1/collections/{coll['name']}/query", json=payload
- )
+ first = await admin_client.post(f"/v1/collections/{coll['name']}/query", json=payload)
first_body = assert_envelope(first, 200)
assert "cache_hit" in first_body["timings"]
- second = await admin_client.post(
- f"/v1/collections/{coll['name']}/query", json=payload
- )
+ second = await admin_client.post(f"/v1/collections/{coll['name']}/query", json=payload)
second_body = assert_envelope(second, 200)
# Cache may or may not hit on the immediate second call depending on
# whether result caching is enabled by the runtime config; just assert
diff --git a/e2e/tests/api/test_sse_events.py b/e2e/tests/api/test_sse_events.py
index 903985a0..9e32c350 100644
--- a/e2e/tests/api/test_sse_events.py
+++ b/e2e/tests/api/test_sse_events.py
@@ -70,7 +70,7 @@ async def _read() -> None:
for trigger in triggers:
await trigger()
await asyncio.wait_for(reader, timeout=timeout)
- except asyncio.TimeoutError as exc:
+ except TimeoutError as exc:
reader.cancel()
raise TimeoutError(
f"SSE stream {path!r} did not satisfy predicate within {timeout}s; "
@@ -109,9 +109,7 @@ async def test_events_token_requires_auth(
async def test_events_token_unknown_collection_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.post(
- f"/v1/collections/{unique_name('missing')}/events/token"
- )
+ resp = await admin_client.post(f"/v1/collections/{unique_name('missing')}/events/token")
assert resp.status_code == 404, resp.text
@@ -173,18 +171,14 @@ async def test_events_stream_requires_auth_without_token(
collection: CollectionFactory,
) -> None:
coll = await collection()
- resp = await unauth_client.get(
- f"/v1/collections/{coll['name']}/events"
- )
+ resp = await unauth_client.get(f"/v1/collections/{coll['name']}/events")
assert resp.status_code == 401, resp.text
async def test_events_stream_unknown_collection_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.get(
- f"/v1/collections/{unique_name('missing')}/events"
- )
+ resp = await admin_client.get(f"/v1/collections/{unique_name('missing')}/events")
assert resp.status_code == 404, resp.text
@@ -194,9 +188,7 @@ async def test_events_stream_token_grants_anonymous_access(
collection: CollectionFactory,
) -> None:
coll = await collection()
- token_resp = await admin_client.post(
- f"/v1/collections/{coll['name']}/events/token"
- )
+ token_resp = await admin_client.post(f"/v1/collections/{coll['name']}/events/token")
token_body = assert_envelope(token_resp, 200)
token = token_body["token"]
diff --git a/e2e/tests/api/test_upload_sessions.py b/e2e/tests/api/test_upload_sessions.py
index dc76c1f9..9931eb55 100644
--- a/e2e/tests/api/test_upload_sessions.py
+++ b/e2e/tests/api/test_upload_sessions.py
@@ -27,7 +27,6 @@
from tests._helpers import assert_envelope, poll_until, unique_name
-
# ---------------------------------------------------------------------------
# Create / Get
# ---------------------------------------------------------------------------
@@ -74,9 +73,7 @@ async def test_get_upload_session_returns_payload(
body = assert_envelope(create, 201)
session_id = body["id"]
- resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/upload-sessions/{session_id}"
- )
+ resp = await admin_client.get(f"/v1/collections/{coll['name']}/upload-sessions/{session_id}")
got = assert_envelope(resp, 200)
assert got["id"] == session_id
assert got["total_files"] == 1
@@ -88,8 +85,7 @@ async def test_get_unknown_upload_session_returns_404(
) -> None:
coll = await collection()
resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/upload-sessions/"
- "00000000-0000-0000-0000-000000000000"
+ f"/v1/collections/{coll['name']}/upload-sessions/00000000-0000-0000-0000-000000000000"
)
assert resp.status_code == 404, resp.text
@@ -99,9 +95,7 @@ async def test_get_invalid_session_uuid_returns_404(
collection: Callable[..., Awaitable[dict[str, Any]]],
) -> None:
coll = await collection()
- resp = await admin_client.get(
- f"/v1/collections/{coll['name']}/upload-sessions/not-a-uuid"
- )
+ resp = await admin_client.get(f"/v1/collections/{coll['name']}/upload-sessions/not-a-uuid")
assert resp.status_code == 404, resp.text
@@ -148,9 +142,7 @@ async def test_full_lifecycle_two_files_end_up_in_collection(
assert complete_body["uploaded_files"] == 2
async def fetch_session() -> dict[str, Any]:
- r = await admin_client.get(
- f"/v1/collections/{coll['name']}/upload-sessions/{session_id}"
- )
+ r = await admin_client.get(f"/v1/collections/{coll['name']}/upload-sessions/{session_id}")
r.raise_for_status()
return r.json()
diff --git a/e2e/tests/api/test_users.py b/e2e/tests/api/test_users.py
index c511183d..1101e34e 100644
--- a/e2e/tests/api/test_users.py
+++ b/e2e/tests/api/test_users.py
@@ -152,9 +152,7 @@ async def test_patch_user_unknown_id_returns_404(
async def test_delete_user_unknown_id_returns_404(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.delete(
- "/v1/admin/users/00000000-0000-0000-0000-000000000000"
- )
+ resp = await admin_client.delete("/v1/admin/users/00000000-0000-0000-0000-000000000000")
assert resp.status_code == 404, resp.text
diff --git a/e2e/tests/api/test_webhooks.py b/e2e/tests/api/test_webhooks.py
index 599964c9..2dd4ef9c 100644
--- a/e2e/tests/api/test_webhooks.py
+++ b/e2e/tests/api/test_webhooks.py
@@ -198,9 +198,7 @@ async def test_test_delivery_reaches_sink_with_valid_signature(
async def test_test_delivery_404_for_missing_webhook(
admin_client: httpx.AsyncClient,
) -> None:
- resp = await admin_client.post(
- "/v1/admin/webhooks/00000000-0000-0000-0000-000000000000/test"
- )
+ resp = await admin_client.post("/v1/admin/webhooks/00000000-0000-0000-0000-000000000000/test")
assert resp.status_code == 404
@@ -255,9 +253,7 @@ async def test_replay_delivery_redelivers(
assert_envelope(resp, 200)
await webhook_sink["wait"](label, count=1)
- body = (
- await admin_client.get(f"/v1/admin/webhooks/{created['id']}/deliveries")
- ).json()
+ body = (await admin_client.get(f"/v1/admin/webhooks/{created['id']}/deliveries")).json()
if not body.get("deliveries"):
pytest.skip("No persisted delivery to replay (test endpoint does not persist)")
diff --git a/e2e/tests/sdk_python/conftest.py b/e2e/tests/sdk_python/conftest.py
index 00c83559..86650d8d 100644
--- a/e2e/tests/sdk_python/conftest.py
+++ b/e2e/tests/sdk_python/conftest.py
@@ -17,7 +17,6 @@
import httpx
import pytest_asyncio
-
from bigrag import BigRAG
SdkClientFactory = Callable[..., Awaitable[BigRAG]]
@@ -97,9 +96,7 @@ async def session_sdk_client(
base_url=api_base_url,
http_client=http_client,
)
- await client.auth.login(
- {"email": admin_setup["email"], "password": admin_setup["password"]}
- )
+ await client.auth.login({"email": admin_setup["email"], "password": admin_setup["password"]})
try:
yield client
finally:
diff --git a/e2e/tests/sdk_python/test_auth.py b/e2e/tests/sdk_python/test_auth.py
index 70a3f1e9..ac89eadd 100644
--- a/e2e/tests/sdk_python/test_auth.py
+++ b/e2e/tests/sdk_python/test_auth.py
@@ -18,8 +18,8 @@
import httpx
import pytest
-
from bigrag import APIError, AuthenticationError, BigRAG, NotFoundError
+
from tests._helpers import unique_name
diff --git a/e2e/tests/sdk_python/test_chat_stream.py b/e2e/tests/sdk_python/test_chat_stream.py
index a24fed52..cad17de8 100644
--- a/e2e/tests/sdk_python/test_chat_stream.py
+++ b/e2e/tests/sdk_python/test_chat_stream.py
@@ -18,7 +18,6 @@
from typing import Any
import pytest
-
from bigrag import BigRAG, NotFoundError
@@ -129,15 +128,11 @@ async def test_chat_client_aliases_create_and_stream(
coll = await collection()
await document(coll["name"], fixture="sample.txt")
- non_stream = await client.chat_create(
- {"collection": coll["name"], "message": "hello"}
- )
+ non_stream = await client.chat_create({"collection": coll["name"], "message": "hello"})
assert non_stream["assistant_message"]["role"] == "assistant"
saw_event = False
- async for event in client.chat_stream(
- {"collection": coll["name"], "message": "hello stream"}
- ):
+ async for event in client.chat_stream({"collection": coll["name"], "message": "hello stream"}):
saw_event = True
if event["event"] == "done":
break
diff --git a/e2e/tests/sdk_python/test_documents.py b/e2e/tests/sdk_python/test_documents.py
index e52c4bfa..fb6c32af 100644
--- a/e2e/tests/sdk_python/test_documents.py
+++ b/e2e/tests/sdk_python/test_documents.py
@@ -18,8 +18,8 @@
from typing import Any
import pytest
-
from bigrag import BigRAG, NotFoundError
+
from tests._helpers import poll_until, read_fixture, unique_name
@@ -83,9 +83,7 @@ async def test_documents_list_and_pagination(
coll = await collection()
doc = await document(coll["name"], fixture="sample.txt")
- listing = await client.documents.list(
- coll["name"], limit=10, offset=0, include_total=True
- )
+ listing = await client.documents.list(coll["name"], limit=10, offset=0, include_total=True)
assert listing["total"] >= 1
ids = [d["id"] for d in listing["documents"]]
assert doc["id"] in ids
diff --git a/e2e/tests/sdk_python/test_errors.py b/e2e/tests/sdk_python/test_errors.py
index 71da2610..6c3ee883 100644
--- a/e2e/tests/sdk_python/test_errors.py
+++ b/e2e/tests/sdk_python/test_errors.py
@@ -22,7 +22,6 @@
from typing import Any
import pytest
-
from bigrag import (
APIConnectionError,
APIError,
@@ -36,6 +35,7 @@
RateLimitError,
error_for_status,
)
+
from tests._helpers import unique_name
@@ -43,9 +43,7 @@ async def test_invalid_api_key_raises_authentication_error(
api_base_url: str,
) -> None:
"""An obviously-bogus bearer token returns 401."""
- async with BigRAG(
- api_key="bigrag_sk_not_a_valid_key_at_all", base_url=api_base_url
- ) as client:
+ async with BigRAG(api_key="bigrag_sk_not_a_valid_key_at_all", base_url=api_base_url) as client:
with pytest.raises(AuthenticationError) as excinfo:
await client.collections.list()
assert excinfo.value.status == 401
@@ -84,9 +82,7 @@ async def test_unknown_webhook_raises_not_found(
) -> None:
# Webhook admin endpoints require session auth, not API key.
with pytest.raises(NotFoundError):
- await session_sdk_client.webhooks.get(
- "00000000-0000-0000-0000-000000000000"
- )
+ await session_sdk_client.webhooks.get("00000000-0000-0000-0000-000000000000")
async def test_validation_error_surfaces_as_api_error(
diff --git a/e2e/tests/sdk_python/test_query.py b/e2e/tests/sdk_python/test_query.py
index 5e790d44..8d5ba8d3 100644
--- a/e2e/tests/sdk_python/test_query.py
+++ b/e2e/tests/sdk_python/test_query.py
@@ -15,8 +15,8 @@
from typing import Any
import pytest
-
from bigrag import BigRAG, NotFoundError
+
from tests._helpers import unique_name
@@ -60,9 +60,7 @@ async def test_query_top_k_limits_result_count(
coll = await collection()
await document(coll["name"], fixture="sample.txt")
- response = await client.queries.query(
- coll["name"], {"query": "sample", "top_k": 1}
- )
+ response = await client.queries.query(coll["name"], {"query": "sample", "top_k": 1})
assert len(response["results"]) <= 1
@@ -135,9 +133,7 @@ async def test_query_empty_collection_returns_empty_results(
client = await sdk_client()
coll = await collection()
- response = await client.queries.query(
- coll["name"], {"query": "anything", "top_k": 5}
- )
+ response = await client.queries.query(coll["name"], {"query": "anything", "top_k": 5})
assert response["results"] == []
assert response["total"] == 0
@@ -203,9 +199,7 @@ async def test_query_unknown_collection_raises_not_found(
) -> None:
client = await sdk_client()
with pytest.raises(NotFoundError):
- await client.queries.query(
- unique_name("missing"), {"query": "x", "top_k": 1}
- )
+ await client.queries.query(unique_name("missing"), {"query": "x", "top_k": 1})
async def test_collection_client_query_wrapper(
diff --git a/sdks/python/src/bigrag/types/collections.py b/sdks/python/src/bigrag/types/collections.py
index 07f1dfb0..93132b9d 100644
--- a/sdks/python/src/bigrag/types/collections.py
+++ b/sdks/python/src/bigrag/types/collections.py
@@ -14,7 +14,6 @@ class Collection(TypedDict):
chunk_size: int
chunk_overlap: int
chunk_strategy: str
- index_type: str
tenant_field: str | None
has_metadata_schema: bool
document_count: int
@@ -58,7 +57,6 @@ class CreateCollectionBody(TypedDict):
chunk_size: NotRequired[int]
chunk_overlap: NotRequired[int]
chunk_strategy: NotRequired[str]
- index_type: NotRequired[str]
tenant_field: NotRequired[str]
metadata_schema: NotRequired[dict[str, Any]]
metadata: NotRequired[dict[str, Any]]
diff --git a/sdks/typescript/src/types/collections.ts b/sdks/typescript/src/types/collections.ts
index e86a0342..af05fd2f 100644
--- a/sdks/typescript/src/types/collections.ts
+++ b/sdks/typescript/src/types/collections.ts
@@ -7,7 +7,6 @@ export interface Collection {
chunk_size: number;
chunk_overlap: number;
chunk_strategy: string;
- index_type: string;
tenant_field: string | null;
has_metadata_schema: boolean;
document_count: number;
@@ -58,7 +57,6 @@ export interface CreateCollectionBody {
chunk_size?: number;
chunk_overlap?: number;
chunk_strategy?: "paragraph" | "recursive";
- index_type?: "HNSW";
tenant_field?: string;
metadata_schema?: Record;
metadata?: Record;
diff --git a/website/content/docs/admin-ui.mdx b/website/content/docs/admin-ui.mdx
index 48061fdf..6ba70afd 100644
--- a/website/content/docs/admin-ui.mdx
+++ b/website/content/docs/admin-ui.mdx
@@ -1,6 +1,6 @@
---
title: Admin UI
-description: Setup, collections, chat, connectors, API keys, usage, audit, webhooks, data storage, vector storage, backups, and settings.
+description: Setup, collections, chat, connectors, API keys, usage, audit, webhooks, data storage, backups, and settings.
---
import { Callout } from "fumadocs-ui/components/callout";
@@ -39,7 +39,7 @@ For cross-site production deployments, enable secure session cookies and usually
| `/collections/[name]/connectors` | Redirects to the first available collection connector from the provider catalog. |
| `/collections/[name]/connectors/google-drive` | Browse Google Drive, sync selected files/folders, monitor Drive sync progress, trigger manual resync, and manage sync schedules. Source creation and manual sync are disabled when the worker is offline. |
| `/collections/[name]/search` | Query tester scoped to the collection — bounded requests, cancel/retry states, result metadata, document filenames, and chunk deep links. |
-| `/collections/[name]/settings` | Edit description, metadata, metadata schema, reranking, chunking, default query params, and collection vector migration. |
+| `/collections/[name]/settings` | Edit description, metadata, metadata schema, reranking, chunking, and default query params. |
| `/chat` | Browser-local playground chat UI; retrieve from one collection, stream cited answers, render safe markdown, and keep local sessions across reloads. |
| `/models` | Manage [embedding presets](/docs/api-reference/embedding-presets), including provider connection tests and OpenAI-compatible base URLs, plus fallback embedding, retrieval, and chat model settings. |
| `/mcp` | Mint, scope, rotate, and revoke [MCP server](/docs/api-reference/mcp-servers) credentials for Claude Desktop / Cursor / custom MCP clients. |
@@ -51,13 +51,12 @@ For cross-site production deployments, enable secure session cookies and usually
| `/connectors` | Browse the provider catalog, configure connector credentials, and review account state. |
| `/backups` | Configure readable backup storage, start full-instance exports, and monitor backup history. Backup starts are disabled when the worker heartbeat is offline or no bucket is configured. |
| `/data-storage` | Configure uploaded source-file storage for local disk or S3-compatible object stores. |
-| `/vector-storage` | Review vector provider health and per-collection routing/stats, configure Qdrant and turbopuffer connection settings, and migrate collections between configured providers. |
| `/settings` | Operator-console settings workspace for account, platform, and data sections. |
| `/evals` | Run golden-set retrieval evaluations. |
Unknown admin routes render the built-in 404 screen with links back to overview and collections. Unexpected route render failures use the admin 500 screen with a retry action, overview fallback, and a readable error-details panel showing the boundary error message, cause, and digest when available.
-The sidebar is grouped by operator task: Workspace for day-to-day retrieval surfaces, Interfaces for client and integration entry points, Observability for logs and spend, and Administration for backup, data storage, vector storage, and instance settings. Member accounts only see groups that still contain at least one available destination. Standard admin pages share one centered dashboard body width so page headers, tabs, and primary panels align across routes; shared tabs rely on the page stack gap so spacing above and below the tab row stays even. The desktop sidebar keeps matching top and bottom insets so it reads as a floating surface, while page scroll areas still keep their own bottom edge available so footer actions are not clipped by a reserved app gutter. The footer account menu opens almost as wide as the sidebar footer trigger, with a small horizontal inset.
+The sidebar is grouped by operator task: Workspace for day-to-day retrieval surfaces, Interfaces for client and integration entry points, Observability for logs and spend, and Administration for backup, data storage, and instance settings. Member accounts only see groups that still contain at least one available destination. Standard admin pages share one centered dashboard body width so page headers, tabs, and primary panels align across routes; shared tabs rely on the page stack gap so spacing above and below the tab row stays even. The desktop sidebar keeps matching top and bottom insets so it reads as a floating surface, while page scroll areas still keep their own bottom edge available so footer actions are not clipped by a reserved app gutter. The footer account menu opens almost as wide as the sidebar footer trigger, with a small horizontal inset.
## Settings sections
@@ -65,22 +64,20 @@ The `/models` page owns reusable embedding presets and runtime model settings. `
The `/connectors` page renders provider tabs above the selected provider setup panel. It owns provider credentials and account state through `GET /v1/admin/connectors/{provider}`, `GET /v1/connectors/{provider}/account`, `PUT /v1/admin/connectors/{provider}`, and `POST /v1/connectors/{provider}/disconnect`. The collection connector pages read from the same provider catalog while staying scoped to browsing, source selection, sync schedules, and sync-job progress.
-The `/backups` page renders backup destination settings directly above export controls backed by `GET /v1/admin/backups`, `GET /v1/admin/realtime/backups`, and `POST /v1/admin/backups` so backup operations stay out of the general Settings workspace. Backup starts are rejected while a vector migration is pending, running, or canceling. Usage, audit, data storage, and vector storage also live as first-class sidebar pages at `/usage`, `/audit`, `/data-storage`, and `/vector-storage`.
+The `/backups` page renders backup destination settings directly above export controls backed by `GET /v1/admin/backups`, `GET /v1/admin/realtime/backups`, and `POST /v1/admin/backups` so backup operations stay out of the general Settings workspace. Usage, audit, and data storage also live as first-class sidebar pages at `/usage`, `/audit`, and `/data-storage`.
The `/data-storage` page is a standalone runtime settings surface for uploaded source-file storage. It uses `GET /v1/admin/settings` and `PUT /v1/admin/settings` for local disk and S3-compatible object-store settings.
-The `/vector-storage` page is a standalone runtime settings surface for vector provider connection details. It separates Qdrant and turbopuffer into provider tabs and uses `GET /v1/admin/settings` and `PUT /v1/admin/settings`; vector migrations use `POST /v1/admin/vector-storage/migrations`, `GET /v1/admin/vector-storage/migrations`, `DELETE /v1/admin/vector-storage/migrations/{migration_id}`, and `GET /v1/admin/realtime/vector-migrations`. A migration pauses writes, copies existing vectors, switches the collection, and deletes old source vectors after cutover. Stop-and-delete remains available while the migration maintenance lock is active.
-
The settings page is a compact task-oriented workspace:
- **Top tab control** for Account, Health, Security, and Data — no nested settings sidebar; each tab opens directly into its area without a repeated section heading.
- **Stacked layout** — settings content is stacked sequentially rather than split into desktop columns. Common controls appear first; raw setting keys and source metadata stay inside Advanced controls; each runtime panel keeps its guidance plus Save action together in the panel footer.
- **Security tab** focuses on trusted-proxy handling, outbound URL policy, and embedding-cache posture. CORS origins and session-cookie deployment controls stay in deployment config and the instance settings API.
-- **Data tab** covers ingestion, queue, retention, and webhook runtime settings after file storage moved to `/data-storage`.
+- **Data tab** covers Turbopuffer, ingestion, queue, retention, and webhook runtime settings after file storage moved to `/data-storage`.
- **Placeholders** — empty controls show default or example values so unset settings remain readable.
-- **Validation** — saving probes applicable storage, backup, and vector targets before persistence.
+- **Validation** — saving probes applicable storage, backup, and Turbopuffer targets before persistence.
-Settings only accepts current tab values: `/settings?tab=account`, `/settings?tab=health`, `/settings?tab=security`, and `/settings?tab=data`. Backups, usage, audit, connectors, models, evals, data storage, and vector storage are standalone routes rather than Settings deep-link redirects.
+Settings only accepts current tab values: `/settings?tab=account`, `/settings?tab=health`, `/settings?tab=security`, and `/settings?tab=data`. Backups, usage, audit, connectors, models, evals, and data storage are standalone routes rather than Settings deep-link redirects.
| Group | Section | Reads from | Writes to |
|-------|---------|-----------|-----------|
From 2d99d80149370444a77ba12aba9a84b2beb8b005 Mon Sep 17 00:00:00 2001
From: Bigint <69431456+bigint@users.noreply.github.com>
Date: Thu, 21 May 2026 23:20:08 +0530
Subject: [PATCH 6/9] Use Turbopuffer SDK and remove Qdrant env example
---
.env.example | 1 -
.../services/vector_store/turbopuffer.py | 79 +--
api/pyproject.toml | 3 +-
api/uv.lock | 611 ++++++++++++++++++
.../content/docs/concepts/architecture.mdx | 5 +
5 files changed, 661 insertions(+), 38 deletions(-)
diff --git a/.env.example b/.env.example
index 7ad0363e..cb643ae2 100644
--- a/.env.example
+++ b/.env.example
@@ -24,7 +24,6 @@ BIGRAG_MASTER_KEY=
# Override only when the backing services are not running on local defaults.
# BIGRAG_DATABASE_URL=postgres://bigrag:bigrag@localhost:5432/bigrag?sslmode=disable
-# BIGRAG_QDRANT_URL=http://localhost:6333
# BIGRAG_REDIS_URL=redis://localhost:6379/0
# Optional Redis password for docker-compose production runs.
diff --git a/api/bigrag/services/vector_store/turbopuffer.py b/api/bigrag/services/vector_store/turbopuffer.py
index aa30e01f..76ab3608 100644
--- a/api/bigrag/services/vector_store/turbopuffer.py
+++ b/api/bigrag/services/vector_store/turbopuffer.py
@@ -2,7 +2,7 @@
from typing import Any
-import httpx
+from turbopuffer import AsyncTurbopuffer, NotFoundError
from bigrag.logging import get_logger
from bigrag.services._retrieval_filters import FilterCondition, FilterExpression
@@ -20,7 +20,7 @@
_EXPORT_PAGE_SIZE = 10000
-def _to_turbopuffer_filter(filters: FilterExpression | None) -> list | None:
+def _to_turbopuffer_filter(filters: FilterExpression | None) -> tuple | None:
if filters is None:
return None
clauses = [_to_turbopuffer_condition(condition) for condition in filters.conditions]
@@ -28,10 +28,10 @@ def _to_turbopuffer_filter(filters: FilterExpression | None) -> list | None:
return None
if len(clauses) == 1:
return clauses[0]
- return ["And", clauses]
+ return ("And", tuple(clauses))
-def _to_turbopuffer_condition(condition: FilterCondition) -> list:
+def _to_turbopuffer_condition(condition: FilterCondition) -> tuple:
field = _PUBLIC_ID_FIELD if condition.field == "id" else condition.field
op = {
"eq": "Eq",
@@ -42,7 +42,7 @@ def _to_turbopuffer_condition(condition: FilterCondition) -> list:
"lte": "Lte",
"in": "In",
}[condition.operator]
- return [field, op, condition.value]
+ return (field, op, condition.value)
def _schema(dimension: int) -> dict:
@@ -65,6 +65,16 @@ def _row_payload(row: dict) -> dict:
return payload
+def _response_row(row: Any) -> dict:
+ if isinstance(row, dict):
+ return row
+ if hasattr(row, "to_dict"):
+ return row.to_dict()
+ if hasattr(row, "model_dump"):
+ return row.model_dump(mode="json", by_alias=True)
+ return dict(row)
+
+
class TurbopufferVectorStore:
def __init__(
self,
@@ -78,25 +88,35 @@ def __init__(
self.region = region
self.prefix = namespace_prefix or "bigrag_"
self.base_url = base_url.rstrip("/") if base_url else None
- self.client: httpx.AsyncClient | None = None
+ self.client: AsyncTurbopuffer | None = None
def connect(self) -> None:
if not self.api_key:
raise RuntimeError("turbopuffer API key is not configured")
- self.client = httpx.AsyncClient(
- base_url=self.base_url or f"https://{self.region}.turbopuffer.com",
- headers={"Authorization": f"Bearer {self.api_key}"},
+ kwargs: dict[str, Any] = {
+ "api_key": self.api_key,
+ }
+ if self.base_url:
+ kwargs["base_url"] = self.base_url
+ else:
+ kwargs["region"] = self.region
+ self.client = AsyncTurbopuffer(
+ **kwargs,
+ max_retries=2,
timeout=30,
)
logger.info("connected to turbopuffer", region=self.region, base_url=self.base_url)
- def _client(self) -> httpx.AsyncClient:
+ def _client(self) -> AsyncTurbopuffer:
if self.client is None:
self.connect()
if self.client is None:
raise RuntimeError("turbopuffer client is not connected")
return self.client
+ def _namespace_client(self, name: str):
+ return self._client().namespace(self._namespace(name))
+
def _namespace(self, name: str) -> str:
return _backend_name(self.prefix, name)
@@ -105,13 +125,12 @@ def _point_id(self, collection: str, value: str) -> str:
async def close(self) -> None:
if self.client is not None:
- await self.client.aclose()
+ await self.client.close()
self.client = None
async def health_check(self) -> None:
- client = self._client()
- response = await client.get("/v1/namespaces")
- response.raise_for_status()
+ async for _ in self._client().namespaces(prefix=self.prefix, page_size=1):
+ break
async def create_collection(
self,
@@ -122,11 +141,10 @@ async def create_collection(
await self.health_check()
async def delete_collection(self, name: str) -> None:
- client = self._client()
- response = await client.delete(f"/v2/namespaces/{self._namespace(name)}")
- if response.status_code == 404:
+ try:
+ await self._namespace_client(name).delete_all()
+ except NotFoundError:
return
- response.raise_for_status()
async def insert(
self,
@@ -167,10 +185,10 @@ async def insert(
return len(rows)
async def _write(self, collection: str, payload: dict) -> dict:
- client = self._client()
- response = await client.post(f"/v2/namespaces/{self._namespace(collection)}", json=payload)
- response.raise_for_status()
- return response.json() if response.content else {}
+ response = await self._namespace_client(collection).write(**payload)
+ if hasattr(response, "to_dict"):
+ return response.to_dict()
+ return {}
async def search(
self,
@@ -192,14 +210,8 @@ async def search(
turbo_filter = _to_turbopuffer_filter(filters)
if turbo_filter:
payload["filters"] = turbo_filter
- client = self._client()
- response = await client.post(
- f"/v2/namespaces/{self._namespace(collection)}/query",
- json=payload,
- )
- response.raise_for_status()
rows = []
- for row in response.json().get("rows", []):
+ for row in await self._query_rows(collection, payload):
point_id = str(row.get("id", ""))
distance = float(row.get("$dist", 0.0))
rows.append(_row_from_payload(point_id, max(0.0, 1.0 - distance), _row_payload(row)))
@@ -235,13 +247,8 @@ async def get_chunks(
return _chunk_rows_from_payloads([_row_payload(row) for row in rows], limit, offset)
async def _query_rows(self, collection: str, payload: dict) -> list[dict]:
- client = self._client()
- response = await client.post(
- f"/v2/namespaces/{self._namespace(collection)}/query",
- json=payload,
- )
- response.raise_for_status()
- return response.json().get("rows", [])
+ response = await self._namespace_client(collection).query(**payload)
+ return [_response_row(row) for row in response.rows]
async def delete_by_document(self, collection: str, document_id: str) -> None:
await self._write(collection, {"delete_by_filter": ["document_id", "Eq", document_id]})
diff --git a/api/pyproject.toml b/api/pyproject.toml
index cf6e33de..f07daee3 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "bigrag"
version = "2026.4.30"
-description = "Self-hostable RAG platform with Docling ingestion and pluggable vector storage"
+description = "Self-hostable RAG platform with Docling ingestion and Turbopuffer vector storage"
license = "MIT"
requires-python = ">=3.12"
dependencies = [
@@ -29,6 +29,7 @@ dependencies = [
"mcp>=1.12.0,<2",
"tiktoken>=0.8.0,<1",
"dramatiq[redis]>=2.1.0,<3",
+ "turbopuffer>=2.1.0,<3",
]
[dependency-groups]
diff --git a/api/uv.lock b/api/uv.lock
index c55d2bb2..f0d0d161 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -31,6 +31,113 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" },
]
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.13.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohappyeyeballs" },
+ { name = "aiosignal" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" },
+ { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" },
+ { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" },
+ { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" },
+ { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" },
+ { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" },
+ { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" },
+ { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" },
+ { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" },
+ { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" },
+ { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" },
+ { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" },
+ { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" },
+ { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" },
+ { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" },
+ { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" },
+ { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" },
+ { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" },
+ { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" },
+ { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" },
+ { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" },
+ { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" },
+ { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" },
+ { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" },
+ { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" },
+ { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" },
+ { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" },
+ { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" },
+ { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" },
+ { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" },
+ { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "frozenlist" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
+]
+
[[package]]
name = "alembic"
version = "1.18.4"
@@ -215,6 +322,7 @@ dependencies = [
{ name = "starlette" },
{ name = "structlog" },
{ name = "tiktoken" },
+ { name = "turbopuffer" },
{ name = "uvicorn", extra = ["standard"] },
]
@@ -248,6 +356,7 @@ requires-dist = [
{ name = "starlette", specifier = ">=1.0.0,<2" },
{ name = "structlog", specifier = ">=24.4.0,<26" },
{ name = "tiktoken", specifier = ">=0.8.0,<1" },
+ { name = "turbopuffer", specifier = ">=2.1.0,<3" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<1" },
]
@@ -873,6 +982,95 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" },
]
+[[package]]
+name = "frozenlist"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
+ { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
+ { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
+ { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
+ { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
+ { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
+ { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
+ { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
+ { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
+ { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
+ { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
+ { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
+ { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
+ { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
+ { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
+ { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
+]
+
[[package]]
name = "fsspec"
version = "2026.4.0"
@@ -1513,6 +1711,105 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
]
+[[package]]
+name = "multidict"
+version = "6.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
+ { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
+ { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
+ { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
+ { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
+ { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
+ { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
+ { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
+ { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
+ { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
+ { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
+ { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
+ { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
+ { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
+ { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
+ { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
+ { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
+ { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
+ { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
+ { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
+ { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
+ { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
+ { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
+ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
+]
+
[[package]]
name = "multiprocess"
version = "0.70.19"
@@ -2019,6 +2316,100 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/34/b6f19941adcdaf415b5e8a8d577499f5b6a76b59cbae37f9b125a9ffe9f2/polyfactory-3.3.0-py3-none-any.whl", hash = "sha256:686abcaa761930d3df87b91e95b26b8d8cb9fdbbbe0b03d5f918acff5c72606e", size = 62707, upload-time = "2026-02-22T09:46:25.985Z" },
]
+[[package]]
+name = "propcache"
+version = "0.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" },
+ { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" },
+ { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" },
+ { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" },
+ { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" },
+ { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" },
+ { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" },
+ { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" },
+ { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" },
+ { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" },
+ { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" },
+ { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" },
+ { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" },
+ { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" },
+ { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" },
+ { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" },
+ { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" },
+ { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" },
+ { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" },
+ { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" },
+ { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" },
+ { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" },
+ { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" },
+ { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" },
+ { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" },
+ { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
+]
+
[[package]]
name = "psutil"
version = "7.2.2"
@@ -2047,6 +2438,124 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
]
+[[package]]
+name = "pybase64"
+version = "1.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" },
+ { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" },
+ { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" },
+ { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" },
+ { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" },
+ { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" },
+ { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" },
+ { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" },
+ { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" },
+ { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394, upload-time = "2025-12-06T13:23:44.317Z" },
+ { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078, upload-time = "2025-12-06T13:23:45.348Z" },
+ { url = "https://files.pythonhosted.org/packages/56/fe/e66fe373bce717c6858427670736d54297938dad61c5907517ab4106bd90/pybase64-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2dc64a94a9d936b8e3449c66afabbaa521d3cc1a563d6bbaaa6ffa4535222e4b", size = 38158, upload-time = "2025-12-06T13:23:46.872Z" },
+ { url = "https://files.pythonhosted.org/packages/80/a9/b806ed1dcc7aed2ea3dd4952286319e6f3a8b48615c8118f453948e01999/pybase64-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e48f86de1c145116ccf369a6e11720ce696c2ec02d285f440dfb57ceaa0a6cb4", size = 31672, upload-time = "2025-12-06T13:23:47.88Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244, upload-time = "2025-12-06T13:23:49.026Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620, upload-time = "2025-12-06T13:23:50.081Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930, upload-time = "2025-12-06T13:23:51.68Z" },
+ { url = "https://files.pythonhosted.org/packages/60/55/e7f7bdcd0fd66e61dda08db158ffda5c89a306bbdaaf5a062fbe4e48f4a1/pybase64-1.4.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:398685a76034e91485a28aeebcb49e64cd663212fd697b2497ac6dfc1df5e671", size = 56425, upload-time = "2025-12-06T13:23:52.732Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/65/b592c7f921e51ca1aca3af5b0d201a98666d0a36b930ebb67e7c2ed27395/pybase64-1.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7e46400a6461187ccb52ed75b0045d937529e801a53a9cd770b350509f9e4d50", size = 59327, upload-time = "2025-12-06T13:23:53.856Z" },
+ { url = "https://files.pythonhosted.org/packages/23/95/1613d2fb82dbb1548595ad4179f04e9a8451bfa18635efce18b631eabe3f/pybase64-1.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1b62b9f2f291d94f5e0b76ab499790b7dcc78a009d4ceea0b0428770267484b6", size = 60294, upload-time = "2025-12-06T13:23:54.937Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/73/40431f37f7d1b3eab4673e7946ff1e8f5d6bd425ec257e834dae8a6fc7b0/pybase64-1.4.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:f30ceb5fa4327809dede614be586efcbc55404406d71e1f902a6fdcf322b93b2", size = 54858, upload-time = "2025-12-06T13:23:56.031Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/84/f6368bcaf9f743732e002a9858646fd7a54f428490d427dd6847c5cfe89e/pybase64-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d5f18ed53dfa1d4cf8b39ee542fdda8e66d365940e11f1710989b3cf4a2ed66", size = 58629, upload-time = "2025-12-06T13:23:57.12Z" },
+ { url = "https://files.pythonhosted.org/packages/43/75/359532f9adb49c6b546cafc65c46ed75e2ccc220d514ba81c686fbd83965/pybase64-1.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:119d31aa4b58b85a8ebd12b63c07681a138c08dfc2fe5383459d42238665d3eb", size = 52448, upload-time = "2025-12-06T13:23:58.298Z" },
+ { url = "https://files.pythonhosted.org/packages/92/6c/ade2ba244c3f33ed920a7ed572ad772eb0b5f14480b72d629d0c9e739a40/pybase64-1.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3cf0218b0e2f7988cf7d738a73b6a1d14f3be6ce249d7c0f606e768366df2cce", size = 68841, upload-time = "2025-12-06T13:23:59.886Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/51/b345139cd236be382f2d4d4453c21ee6299e14d2f759b668e23080f8663f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:12f4ee5e988bc5c0c1106b0d8fc37fb0508f12dab76bac1b098cb500d148da9d", size = 57910, upload-time = "2025-12-06T13:24:00.994Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/b8/9f84bdc4f1c4f0052489396403c04be2f9266a66b70c776001eaf0d78c1f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:937826bc7b6b95b594a45180e81dd4d99bd4dd4814a443170e399163f7ff3fb6", size = 54335, upload-time = "2025-12-06T13:24:02.046Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/c7/be63b617d284de46578a366da77ede39c8f8e815ed0d82c7c2acca560fab/pybase64-1.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:88995d1460971ef80b13e3e007afbe4b27c62db0508bc7250a2ab0a0b4b91362", size = 56486, upload-time = "2025-12-06T13:24:03.141Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/96/f252c8f9abd6ded3ef1ccd3cdbb8393a33798007f761b23df8de1a2480e6/pybase64-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:72326fe163385ed3e1e806dd579d47fde5d8a59e51297a60fc4e6cbc1b4fc4ed", size = 70978, upload-time = "2025-12-06T13:24:04.221Z" },
+ { url = "https://files.pythonhosted.org/packages/af/51/0f5714af7aeef96e30f968e4371d75ad60558aaed3579d7c6c8f1c43c18a/pybase64-1.4.3-cp313-cp313-win32.whl", hash = "sha256:b1623730c7892cf5ed0d6355e375416be6ef8d53ab9b284f50890443175c0ac3", size = 33684, upload-time = "2025-12-06T13:24:05.29Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ad/0cea830a654eb08563fb8214150ef57546ece1cc421c09035f0e6b0b5ea9/pybase64-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:8369887590f1646a5182ca2fb29252509da7ae31d4923dbb55d3e09da8cc4749", size = 35832, upload-time = "2025-12-06T13:24:06.35Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/0d/eec2a8214989c751bc7b4cad1860eb2c6abf466e76b77508c0f488c96a37/pybase64-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:860b86bca71e5f0237e2ab8b2d9c4c56681f3513b1bf3e2117290c1963488390", size = 31175, upload-time = "2025-12-06T13:24:07.419Z" },
+ { url = "https://files.pythonhosted.org/packages/db/c9/e23463c1a2913686803ef76b1a5ae7e6fac868249a66e48253d17ad7232c/pybase64-1.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eb51db4a9c93215135dccd1895dca078e8785c357fabd983c9f9a769f08989a9", size = 38497, upload-time = "2025-12-06T13:24:08.873Z" },
+ { url = "https://files.pythonhosted.org/packages/71/83/343f446b4b7a7579bf6937d2d013d82f1a63057cf05558e391ab6039d7db/pybase64-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a03ef3f529d85fd46b89971dfb00c634d53598d20ad8908fb7482955c710329d", size = 32076, upload-time = "2025-12-06T13:24:09.975Z" },
+ { url = "https://files.pythonhosted.org/packages/46/fc/cb64964c3b29b432f54d1bce5e7691d693e33bbf780555151969ffd95178/pybase64-1.4.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2e745f2ce760c6cf04d8a72198ef892015ddb89f6ceba489e383518ecbdb13ab", size = 72317, upload-time = "2025-12-06T13:24:11.129Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/b7/fab2240da6f4e1ad46f71fa56ec577613cf5df9dce2d5b4cfaa4edd0e365/pybase64-1.4.3-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fac217cd9de8581a854b0ac734c50fd1fa4b8d912396c1fc2fce7c230efe3a7", size = 75534, upload-time = "2025-12-06T13:24:12.433Z" },
+ { url = "https://files.pythonhosted.org/packages/91/3b/3e2f2b6e68e3d83ddb9fa799f3548fb7449765daec9bbd005a9fbe296d7f/pybase64-1.4.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da1ee8fa04b283873de2d6e8fa5653e827f55b86bdf1a929c5367aaeb8d26f8a", size = 65399, upload-time = "2025-12-06T13:24:13.928Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/08/476ac5914c3b32e0274a2524fc74f01cbf4f4af4513d054e41574eb018f6/pybase64-1.4.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b0bf8e884ee822ca7b1448eeb97fa131628fe0ff42f60cae9962789bd562727f", size = 60487, upload-time = "2025-12-06T13:24:15.177Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/b8/618a92915330cc9cba7880299b546a1d9dab1a21fd6c0292ee44a4fe608c/pybase64-1.4.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1bf749300382a6fd1f4f255b183146ef58f8e9cb2f44a077b3a9200dfb473a77", size = 63959, upload-time = "2025-12-06T13:24:16.854Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/52/af9d8d051652c3051862c442ec3861259c5cdb3fc69774bc701470bd2a59/pybase64-1.4.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:153a0e42329b92337664cfc356f2065248e6c9a1bd651bbcd6dcaf15145d3f06", size = 64874, upload-time = "2025-12-06T13:24:18.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/51/5381a7adf1f381bd184d33203692d3c57cf8ae9f250f380c3fecbdbe554b/pybase64-1.4.3-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:86ee56ac7f2184ca10217ed1c655c1a060273e233e692e9086da29d1ae1768db", size = 58572, upload-time = "2025-12-06T13:24:19.417Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/f0/578ee4ffce5818017de4fdf544e066c225bc435e73eb4793cde28a689d0b/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0e71a4db76726bf830b47477e7d830a75c01b2e9b01842e787a0836b0ba741e3", size = 63636, upload-time = "2025-12-06T13:24:20.497Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/ad/8ae94814bf20159ea06310b742433e53d5820aa564c9fdf65bf2d79f8799/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2ba7799ec88540acd9861b10551d24656ca3c2888ecf4dba2ee0a71544a8923f", size = 56193, upload-time = "2025-12-06T13:24:21.559Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/31/6438cfcc3d3f0fa84d229fa125c243d5094e72628e525dfefadf3bcc6761/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2860299e4c74315f5951f0cf3e72ba0f201c3356c8a68f95a3ab4e620baf44e9", size = 72655, upload-time = "2025-12-06T13:24:22.673Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/0d/2bbc9e9c3fc12ba8a6e261482f03a544aca524f92eae0b4908c0a10ba481/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:bb06015db9151f0c66c10aae8e3603adab6b6cd7d1f7335a858161d92fc29618", size = 62471, upload-time = "2025-12-06T13:24:23.8Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0b/34d491e7f49c1dbdb322ea8da6adecda7c7cd70b6644557c6e4ca5c6f7c7/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:242512a070817272865d37c8909059f43003b81da31f616bb0c391ceadffe067", size = 58119, upload-time = "2025-12-06T13:24:24.994Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/17/c21d0cde2a6c766923ae388fc1f78291e1564b0d38c814b5ea8a0e5e081c/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5d8277554a12d3e3eed6180ebda62786bf9fc8d7bb1ee00244258f4a87ca8d20", size = 60791, upload-time = "2025-12-06T13:24:26.046Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b2/eaa67038916a48de12b16f4c384bcc1b84b7ec731b23613cb05f27673294/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f40b7ddd698fc1e13a4b64fbe405e4e0e1279e8197e37050e24154655f5f7c4e", size = 74701, upload-time = "2025-12-06T13:24:27.466Z" },
+ { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965, upload-time = "2025-12-06T13:24:28.548Z" },
+ { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207, upload-time = "2025-12-06T13:24:29.646Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505, upload-time = "2025-12-06T13:24:30.687Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" },
+ { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" },
+ { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/d4/6c0e0cf0efd53c254173fbcd84a3d8fcbf5e0f66622473da425becec32a5/pybase64-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:720104fd7303d07bac302be0ff8f7f9f126f2f45c1edb4f48fdb0ff267e69fe1", size = 38257, upload-time = "2025-12-06T13:24:38.049Z" },
+ { url = "https://files.pythonhosted.org/packages/50/eb/27cb0b610d5cd70f5ad0d66c14ad21c04b8db930f7139818e8fbdc14df4d/pybase64-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83f1067f73fa5afbc3efc0565cecc6ed53260eccddef2ebe43a8ce2b99ea0e0a", size = 31685, upload-time = "2025-12-06T13:24:40.327Z" },
+ { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" },
+ { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/68/733324e28068a89119af2921ce548e1c607cc5c17d354690fc51c302e326/pybase64-1.4.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:acb61f5ab72bec808eb0d4ce8b87ec9f38d7d750cb89b1371c35eb8052a29f11", size = 56478, upload-time = "2025-12-06T13:24:45.815Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/9e/f3f4aa8cfe3357a3cdb0535b78eb032b671519d3ecc08c58c4c6b72b5a91/pybase64-1.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:2bc2d5bc15168f5c04c53bdfe5a1e543b2155f456ed1e16d7edce9ce73842021", size = 59463, upload-time = "2025-12-06T13:24:46.938Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/d1/53286038e1f0df1cf58abcf4a4a91b0f74ab44539c2547b6c31001ddd054/pybase64-1.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8a7bc3cd23880bdca59758bcdd6f4ef0674f2393782763910a7466fab35ccb98", size = 60360, upload-time = "2025-12-06T13:24:48.039Z" },
+ { url = "https://files.pythonhosted.org/packages/00/9a/5cc6ce95db2383d27ff4d790b8f8b46704d360d701ab77c4f655bcfaa6a7/pybase64-1.4.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ad15acf618880d99792d71e3905b0e2508e6e331b76a1b34212fa0f11e01ad28", size = 54999, upload-time = "2025-12-06T13:24:49.547Z" },
+ { url = "https://files.pythonhosted.org/packages/64/e7/c3c1d09c3d7ae79e3aa1358c6d912d6b85f29281e47aa94fc0122a415a2f/pybase64-1.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448158d417139cb4851200e5fee62677ae51f56a865d50cda9e0d61bda91b116", size = 58736, upload-time = "2025-12-06T13:24:50.641Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d5/0baa08e3d8119b15b588c39f0d39fd10472f0372e3c54ca44649cbefa256/pybase64-1.4.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9058c49b5a2f3e691b9db21d37eb349e62540f9f5fc4beabf8cbe3c732bead86", size = 52298, upload-time = "2025-12-06T13:24:51.791Z" },
+ { url = "https://files.pythonhosted.org/packages/00/87/fc6f11474a1de7e27cd2acbb8d0d7508bda3efa73dfe91c63f968728b2a3/pybase64-1.4.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ce561724f6522907a66303aca27dce252d363fcd85884972d348f4403ba3011a", size = 69049, upload-time = "2025-12-06T13:24:53.253Z" },
+ { url = "https://files.pythonhosted.org/packages/69/9d/7fb5566f669ac18b40aa5fc1c438e24df52b843c1bdc5da47d46d4c1c630/pybase64-1.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:63316560a94ac449fe86cb8b9e0a13714c659417e92e26a5cbf085cd0a0c838d", size = 57952, upload-time = "2025-12-06T13:24:54.342Z" },
+ { url = "https://files.pythonhosted.org/packages/de/cc/ceb949232dbbd3ec4ee0190d1df4361296beceee9840390a63df8bc31784/pybase64-1.4.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ecd796f2ac0be7b73e7e4e232b8c16422014de3295d43e71d2b19fd4a4f5368", size = 54484, upload-time = "2025-12-06T13:24:55.774Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/69/659f3c8e6a5d7b753b9c42a4bd9c42892a0f10044e9c7351a4148d413a33/pybase64-1.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d01e102a12fb2e1ed3dc11611c2818448626637857ec3994a9cf4809dfd23477", size = 56542, upload-time = "2025-12-06T13:24:57Z" },
+ { url = "https://files.pythonhosted.org/packages/85/2c/29c9e6c9c82b72025f9676f9e82eb1fd2339ad038cbcbf8b9e2ac02798fc/pybase64-1.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ebff797a93c2345f22183f454fd8607a34d75eca5a3a4a969c1c75b304cee39d", size = 71045, upload-time = "2025-12-06T13:24:58.179Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/84/5a3dce8d7a0040a5c0c14f0fe1311cd8db872913fa04438071b26b0dac04/pybase64-1.4.3-cp314-cp314-win32.whl", hash = "sha256:28b2a1bb0828c0595dc1ea3336305cd97ff85b01c00d81cfce4f92a95fb88f56", size = 34200, upload-time = "2025-12-06T13:24:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/57/bc/ce7427c12384adee115b347b287f8f3cf65860b824d74fe2c43e37e81c1f/pybase64-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:33338d3888700ff68c3dedfcd49f99bfc3b887570206130926791e26b316b029", size = 36323, upload-time = "2025-12-06T13:25:01.708Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/1b/2b8ffbe9a96eef7e3f6a5a7be75995eebfb6faaedc85b6da6b233e50c778/pybase64-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:62725669feb5acb186458da2f9353e88ae28ef66bb9c4c8d1568b12a790dfa94", size = 31584, upload-time = "2025-12-06T13:25:02.801Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/d8/6824c2e6fb45b8fa4e7d92e3c6805432d5edc7b855e3e8e1eedaaf6efb7c/pybase64-1.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:153fe29be038948d9372c3e77ae7d1cab44e4ba7d9aaf6f064dbeea36e45b092", size = 38601, upload-time = "2025-12-06T13:25:04.222Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/e5/10d2b3a4ad3a4850be2704a2f70cd9c0cf55725c8885679872d3bc846c67/pybase64-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7fe3decaa7c4a9e162327ec7bd81ce183d2b16f23c6d53b606649c6e0203e9e", size = 32078, upload-time = "2025-12-06T13:25:05.362Z" },
+ { url = "https://files.pythonhosted.org/packages/43/04/8b15c34d3c2282f1c1b0850f1113a249401b618a382646a895170bc9b5e7/pybase64-1.4.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a5ae04ea114c86eb1da1f6e18d75f19e3b5ae39cb1d8d3cd87c29751a6a22780", size = 72474, upload-time = "2025-12-06T13:25:06.434Z" },
+ { url = "https://files.pythonhosted.org/packages/42/00/f34b4d11278f8fdc68bc38f694a91492aa318f7c6f1bd7396197ac0f8b12/pybase64-1.4.3-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1755b3dce3a2a5c7d17ff6d4115e8bee4a1d5aeae74469db02e47c8f477147da", size = 75706, upload-time = "2025-12-06T13:25:07.636Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/5d/71747d4ad7fe16df4c4c852bdbdeb1f2cf35677b48d7c34d3011a7a6ad3a/pybase64-1.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb852f900e27ffc4ec1896817535a0fa19610ef8875a096b59f21d0aa42ff172", size = 65589, upload-time = "2025-12-06T13:25:08.809Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b1/d1e82bd58805bb5a3a662864800bab83a83a36ba56e7e3b1706c708002a5/pybase64-1.4.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9cf21ea8c70c61eddab3421fbfce061fac4f2fb21f7031383005a1efdb13d0b9", size = 60670, upload-time = "2025-12-06T13:25:10.04Z" },
+ { url = "https://files.pythonhosted.org/packages/15/67/16c609b7a13d1d9fc87eca12ba2dce5e67f949eeaab61a41bddff843cbb0/pybase64-1.4.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:afff11b331fdc27692fc75e85ae083340a35105cea1a3c4552139e2f0e0d174f", size = 64194, upload-time = "2025-12-06T13:25:11.48Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/11/37bc724e42960f0106c2d33dc957dcec8f760c91a908cc6c0df7718bc1a8/pybase64-1.4.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9a5143df542c1ce5c1f423874b948c4d689b3f05ec571f8792286197a39ba02", size = 64984, upload-time = "2025-12-06T13:25:12.645Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/66/b2b962a6a480dd5dae3029becf03ea1a650d326e39bf1c44ea3db78bb010/pybase64-1.4.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d62e9861019ad63624b4a7914dff155af1cc5d6d79df3be14edcaedb5fdad6f9", size = 58750, upload-time = "2025-12-06T13:25:13.848Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/15/9b6d711035e29b18b2e1c03d47f41396d803d06ef15b6c97f45b75f73f04/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:84cfd4d92668ef5766cc42a9c9474b88960ac2b860767e6e7be255c6fddbd34a", size = 63816, upload-time = "2025-12-06T13:25:15.356Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/21/e2901381ed0df62e2308380f30d9c4d87d6b74e33a84faed3478d33a7197/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:60fc025437f9a7c2cc45e0c19ed68ed08ba672be2c5575fd9d98bdd8f01dd61f", size = 56348, upload-time = "2025-12-06T13:25:16.559Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/16/3d788388a178a0407aa814b976fe61bfa4af6760d9aac566e59da6e4a8b4/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edc8446196f04b71d3af76c0bd1fe0a45066ac5bffecca88adb9626ee28c266f", size = 72842, upload-time = "2025-12-06T13:25:18.055Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/63/c15b1f8bd47ea48a5a2d52a4ec61f037062932ea6434ab916107b58e861e/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e99f6fa6509c037794da57f906ade271f52276c956d00f748e5b118462021d48", size = 62651, upload-time = "2025-12-06T13:25:19.191Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/b8/f544a2e37c778d59208966d4ef19742a0be37c12fc8149ff34483c176616/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d94020ef09f624d841aa9a3a6029df8cf65d60d7a6d5c8687579fa68bd679b65", size = 58295, upload-time = "2025-12-06T13:25:20.822Z" },
+ { url = "https://files.pythonhosted.org/packages/03/99/1fae8a3b7ac181e36f6e7864a62d42d5b1f4fa7edf408c6711e28fba6b4d/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f64ce70d89942a23602dee910dec9b48e5edf94351e1b378186b74fcc00d7f66", size = 60960, upload-time = "2025-12-06T13:25:22.099Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/9e/cd4c727742345ad8384569a4466f1a1428f4e5cc94d9c2ab2f53d30be3fe/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ea99f56e45c469818b9781903be86ba4153769f007ba0655fa3b46dc332803d", size = 74863, upload-time = "2025-12-06T13:25:23.442Z" },
+ { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" },
+ { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" },
+ { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" },
+ { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" },
+]
+
[[package]]
name = "pyclipper"
version = "1.4.0"
@@ -3259,6 +3768,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/68/fa86e5a39608000f645535b2c124920126327ab731f8c4fafd5b07ff8d4b/triton-3.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce061073102714b725f3660ec6939d94a1da7984b3aa99c921417cae273672f5", size = 201546766, upload-time = "2026-05-07T18:46:42.088Z" },
]
+[[package]]
+name = "turbopuffer"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "orjson" },
+ { name = "pybase64" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/af/6c/00fb1c2bc74e2c61e7185649f1439aadb43f28362907d4f2a7eea4030559/turbopuffer-2.1.0.tar.gz", hash = "sha256:c9b9447cc30ec54838b390ae07dd584d77d54c7fbf4df4482559279a767eb044", size = 345993, upload-time = "2026-05-17T21:00:56.658Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2b/fb/ca540afa9134e69b2280d4d52947874d271c10397006f4c06f07542b9d97/turbopuffer-2.1.0-py3-none-any.whl", hash = "sha256:63edcf7ef5d4a2ad0283cd0fbbf374732ff073107c26ae63ec2dd882f5abb422", size = 130463, upload-time = "2026-05-17T21:00:54.958Z" },
+]
+
[[package]]
name = "typer"
version = "0.21.2"
@@ -3520,3 +4049,85 @@ sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" },
]
+
+[[package]]
+name = "yarl"
+version = "1.24.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" },
+ { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" },
+ { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" },
+ { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" },
+ { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" },
+ { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" },
+ { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" },
+ { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" },
+ { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" },
+ { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" },
+ { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" },
+ { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" },
+ { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" },
+ { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" },
+ { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" },
+ { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" },
+ { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" },
+ { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" },
+ { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" },
+ { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" },
+ { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" },
+ { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" },
+ { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" },
+]
diff --git a/website/content/docs/concepts/architecture.mdx b/website/content/docs/concepts/architecture.mdx
index 33e701f9..c89eef45 100644
--- a/website/content/docs/concepts/architecture.mdx
+++ b/website/content/docs/concepts/architecture.mdx
@@ -29,6 +29,11 @@ bigRAG runs Postgres and Redis locally with `docker compose up`, while
Turbopuffer provides the vector and full-text search backend.
+The backend talks to Turbopuffer through the official Python client for
+namespace writes, deletes, health listing, vector queries, and BM25
+queries. The local e2e stack points that same client at the fake
+Turbopuffer service through `turbopuffer_base_url`.
+
The admin UI leans on TanStack Router's built-in route code splitting
instead of wrapping dashboard pages in extra `React.lazy` boundaries,
so each route transition pays for one route chunk, not a route chunk
From a63f1e574ce9b3fa08b93cd8e2a72dd23badd2e7 Mon Sep 17 00:00:00 2001
From: Bigint <69431456+bigint@users.noreply.github.com>
Date: Thu, 21 May 2026 23:24:34 +0530
Subject: [PATCH 7/9] chore: make turbopuffer runtime-configured
---
README.md | 8 +--
api/bigrag/config.py | 5 --
api/bigrag/services/jobs/runtime.py | 72 ++++++++++++++-----
api/bigrag/services/vector_store/facade.py | 8 +--
.../services/vector_store/turbopuffer.py | 4 +-
bigrag.toml | 5 --
dev.sh | 4 --
docker-compose.yml | 4 --
e2e/docker-compose.e2e.yml | 8 ---
website/content/docs/deployment/docker.mdx | 10 +--
.../content/docs/deployment/encryption.mdx | 2 +-
.../content/docs/deployment/production.mdx | 15 +---
website/content/docs/deployment/railway.mdx | 16 ++---
.../docs/getting-started/configuration.mdx | 11 +--
14 files changed, 72 insertions(+), 100 deletions(-)
diff --git a/README.md b/README.md
index bf07a538..e269dbf3 100644
--- a/README.md
+++ b/README.md
@@ -288,8 +288,8 @@ Full-workspace keys expose 8 tools — `list_collections`, `get_collection`, `ge
## Configuration
-Most settings use the `BIGRAG_` prefix as environment variables, or configure via `bigrag.toml`.
-Backend logging defaults to `debug` / `text` for local development. Use `BIGRAG_LOG_LEVEL=info` and `BIGRAG_LOG_FORMAT=json` for production log collection.
+Bootstrap settings use the `BIGRAG_` prefix as environment variables, or configure via `bigrag.toml`.
+Backend logging defaults to `debug` / `text` for local development. Use `BIGRAG_LOG_LEVEL=info` and `BIGRAG_LOG_FORMAT=json` for production log collection. Configure Turbopuffer from the admin UI; it is stored in Postgres with the other instance settings.
| Variable | Description | Default |
|----------|-------------|---------|
@@ -303,10 +303,6 @@ Backend logging defaults to `debug` / `text` for local development. Use `BIGRAG_
| `BIGRAG_DB_POOL_MIN` | Min Postgres pool size | `5` |
| `BIGRAG_DB_POOL_MAX` | Max Postgres pool size | `50` |
| `BIGRAG_MIGRATION_TIMEOUT_SECONDS` | Startup migration check timeout (`0` disables the timeout) | `60` |
-| `BIGRAG_TURBOPUFFER_API_KEY` | Turbopuffer API key | — |
-| `BIGRAG_TURBOPUFFER_REGION` | Turbopuffer region | `aws-us-east-1` |
-| `BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX` | Prefix prepended to Turbopuffer namespace names | `bigrag_` |
-| `BIGRAG_TURBOPUFFER_BASE_URL` | Optional Turbopuffer-compatible endpoint for local/e2e services | — |
| `BIGRAG_REDIS_URL` | Redis URL | `redis://localhost:6379/0` |
| `BIGRAG_ENV` | `dev` or `prod` (prod enables startup safety checks) | `dev` |
| `BIGRAG_TRUSTED_PROXIES` | JSON array of trusted proxy CIDRs used to honor `X-Forwarded-For` for audit and access logs | `[]` |
diff --git a/api/bigrag/config.py b/api/bigrag/config.py
index 5731b974..b8c81c3f 100644
--- a/api/bigrag/config.py
+++ b/api/bigrag/config.py
@@ -27,11 +27,6 @@ class Settings(BaseSettings):
db_pool_max: int = 50
migration_timeout_seconds: int = 60
- turbopuffer_api_key: str | None = None
- turbopuffer_base_url: str | None = None
- turbopuffer_region: str = "aws-us-east-1"
- turbopuffer_namespace_prefix: str = "bigrag_"
-
redis_url: str = "redis://localhost:6379/0"
master_key: str | None = None
diff --git a/api/bigrag/services/jobs/runtime.py b/api/bigrag/services/jobs/runtime.py
index 4214654d..e569385d 100644
--- a/api/bigrag/services/jobs/runtime.py
+++ b/api/bigrag/services/jobs/runtime.py
@@ -3,6 +3,7 @@
import asyncio
import os
from datetime import UTC, datetime
+from typing import Any
from bigrag import __version__
from bigrag import config as config_module
@@ -31,8 +32,16 @@
_initialized = False
_storage = None
_heartbeat_task: asyncio.Task | None = None
+_vector_store_settings: tuple[object, ...] | None = None
_HEARTBEAT_SECONDS = 30
_HEARTBEAT_TTL_SECONDS = 120
+_RUNTIME_SETTING_KEYS = (
+ "ingestion_workers",
+ "turbopuffer_api_key",
+ "turbopuffer_base_url",
+ "turbopuffer_namespace_prefix",
+ "turbopuffer_region",
+)
_DEFAULT_QUEUES = {
INGESTION_QUEUE,
CONNECTORS_QUEUE,
@@ -46,6 +55,7 @@ async def ensure_worker_runtime() -> None:
global _initialized, _storage
async with _lock:
if _initialized:
+ await _sync_runtime_settings()
await start_worker_heartbeat()
return
settings = config_module.settings
@@ -61,25 +71,9 @@ async def ensure_worker_runtime() -> None:
await run_migrations()
configure_logging(log_level=settings.log_level, log_format=settings.log_format)
logger.info("worker migrations complete")
- runtime = await runtime_settings.get_values(
- [
- "ingestion_workers",
- "turbopuffer_api_key",
- "turbopuffer_base_url",
- "turbopuffer_namespace_prefix",
- "turbopuffer_region",
- ]
- )
+ runtime = await runtime_settings.get_values(list(_RUNTIME_SETTING_KEYS))
logger.info("worker runtime settings loaded")
- vector_store.configure(
- turbopuffer_api_key=runtime["turbopuffer_api_key"],
- turbopuffer_base_url=runtime["turbopuffer_base_url"],
- turbopuffer_region=runtime["turbopuffer_region"],
- turbopuffer_namespace_prefix=runtime["turbopuffer_namespace_prefix"],
- )
- vector_store.connect()
- await vector_store.health_check()
- logger.info("worker vector store ready")
+ await _sync_vector_store(runtime)
_storage = await init_storage_from_runtime(upload_dir=settings.upload_dir)
await redis_cache.connect(settings.redis_url)
await event_bus.connect(settings.redis_url)
@@ -94,6 +88,48 @@ async def ensure_worker_runtime() -> None:
logger.info("worker ready")
+async def _sync_runtime_settings() -> None:
+ runtime = await runtime_settings.get_values(list(_RUNTIME_SETTING_KEYS))
+ ingestion_queue._num_workers = runtime["ingestion_workers"]
+ await _sync_vector_store(runtime)
+
+
+async def _sync_vector_store(runtime: dict[str, Any]) -> None:
+ global _vector_store_settings
+ settings = (
+ runtime["turbopuffer_api_key"],
+ runtime["turbopuffer_base_url"],
+ runtime["turbopuffer_namespace_prefix"],
+ runtime["turbopuffer_region"],
+ )
+ if settings == _vector_store_settings:
+ return
+ if _vector_store_settings is not None:
+ await vector_store.close()
+ vector_store.configure(
+ turbopuffer_api_key=runtime["turbopuffer_api_key"],
+ turbopuffer_base_url=runtime["turbopuffer_base_url"],
+ turbopuffer_region=runtime["turbopuffer_region"],
+ turbopuffer_namespace_prefix=runtime["turbopuffer_namespace_prefix"],
+ )
+ if runtime["turbopuffer_api_key"]:
+ try:
+ vector_store.connect()
+ await vector_store.health_check()
+ logger.info("worker vector store ready")
+ except Exception as exc:
+ logger.warning(
+ "worker vector store connection failed; worker will continue degraded",
+ provider="turbopuffer",
+ error_type=exc.__class__.__name__,
+ error=str(exc),
+ )
+ else:
+ logger.warning("worker vector store not configured", provider="turbopuffer")
+ _vector_store_settings = settings
+ ingestion_queue.bind_vector_store(vector_store)
+
+
async def record_worker_heartbeat() -> None:
redis = ingestion_queue.redis
if redis is not None:
diff --git a/api/bigrag/services/vector_store/facade.py b/api/bigrag/services/vector_store/facade.py
index 1271a52a..055e5fa3 100644
--- a/api/bigrag/services/vector_store/facade.py
+++ b/api/bigrag/services/vector_store/facade.py
@@ -5,7 +5,6 @@
from contextlib import asynccontextmanager
from typing import Any
-from bigrag.config import settings as _app_settings
from bigrag.services._retrieval_filters import FilterExpression
from bigrag.services.vector_store.base import VectorStoreBackend
from bigrag.services.vector_store.turbopuffer import TurbopufferVectorStore
@@ -13,12 +12,7 @@
class VectorStore:
def __init__(self) -> None:
- self._backend_instance: VectorStoreBackend = TurbopufferVectorStore(
- api_key=_app_settings.turbopuffer_api_key,
- region=_app_settings.turbopuffer_region,
- namespace_prefix=_app_settings.turbopuffer_namespace_prefix,
- base_url=_app_settings.turbopuffer_base_url,
- )
+ self._backend_instance: VectorStoreBackend = TurbopufferVectorStore()
self.client: Any | None = None
self._condition = asyncio.Condition()
self._active = 0
diff --git a/api/bigrag/services/vector_store/turbopuffer.py b/api/bigrag/services/vector_store/turbopuffer.py
index 76ab3608..0cfefd63 100644
--- a/api/bigrag/services/vector_store/turbopuffer.py
+++ b/api/bigrag/services/vector_store/turbopuffer.py
@@ -79,8 +79,8 @@ class TurbopufferVectorStore:
def __init__(
self,
*,
- api_key: str | None,
- region: str,
+ api_key: str | None = None,
+ region: str = "aws-us-east-1",
namespace_prefix: str = "bigrag_",
base_url: str | None = None,
) -> None:
diff --git a/bigrag.toml b/bigrag.toml
index 0a150487..0db76471 100644
--- a/bigrag.toml
+++ b/bigrag.toml
@@ -21,11 +21,6 @@
# db_pool_max = 50
# migration_timeout_seconds = 60
-# turbopuffer_api_key = "" # required for Turbopuffer-backed search
-# turbopuffer_region = "aws-us-east-1"
-# turbopuffer_namespace_prefix = "bigrag_"
-# turbopuffer_base_url = "" # optional Turbopuffer-compatible endpoint
-
# redis_url = "redis://localhost:6379/0"
# Encryption key for provider secrets at rest. Required in env=prod.
diff --git a/dev.sh b/dev.sh
index 467a7a19..9be28a5b 100755
--- a/dev.sh
+++ b/dev.sh
@@ -147,10 +147,6 @@ if [ "$START_BACKEND" = true ]; then
export BIGRAG_DATABASE_URL="$DATABASE_URL"
export BIGRAG_REDIS_URL="$REDIS_URL"
- export BIGRAG_TURBOPUFFER_API_KEY="${BIGRAG_TURBOPUFFER_API_KEY:-}"
- export BIGRAG_TURBOPUFFER_REGION="${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}"
- export BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX="${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}"
- export BIGRAG_TURBOPUFFER_BASE_URL="${BIGRAG_TURBOPUFFER_BASE_URL:-}"
export BIGRAG_MASTER_KEY="$DEV_MASTER_KEY"
export BIGRAG_CORS_ORIGINS="${BIGRAG_CORS_ORIGINS:-[\"http://localhost:3000\"]}"
export PYTHONUNBUFFERED=1
diff --git a/docker-compose.yml b/docker-compose.yml
index 4fdce40c..ff8801dc 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,10 +2,6 @@ x-bigrag-env: &bigrag-env
BIGRAG_ENV: "${BIGRAG_ENV:-dev}"
BIGRAG_DATABASE_URL: "postgres://${POSTGRES_USER:-bigrag}:${POSTGRES_PASSWORD:-bigrag}@postgres:5432/${POSTGRES_DB:-bigrag}?sslmode=disable"
BIGRAG_REDIS_URL: "${BIGRAG_REDIS_URL:-redis://redis:6379/0}"
- BIGRAG_TURBOPUFFER_API_KEY: "${BIGRAG_TURBOPUFFER_API_KEY:-}"
- BIGRAG_TURBOPUFFER_REGION: "${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}"
- BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: "${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}"
- BIGRAG_TURBOPUFFER_BASE_URL: "${BIGRAG_TURBOPUFFER_BASE_URL:-}"
BIGRAG_MASTER_KEY: "${BIGRAG_MASTER_KEY:-}"
BIGRAG_MASTER_KEY_PREVIOUS: '${BIGRAG_MASTER_KEY_PREVIOUS:-[]}'
BIGRAG_SESSION_COOKIE_SECURE: "${BIGRAG_SESSION_COOKIE_SECURE:-false}"
diff --git a/e2e/docker-compose.e2e.yml b/e2e/docker-compose.e2e.yml
index 0c9b0b20..006c9628 100644
--- a/e2e/docker-compose.e2e.yml
+++ b/e2e/docker-compose.e2e.yml
@@ -24,10 +24,6 @@ services:
BIGRAG_CHAT_BASE_URL: http://fake-openai:9001/v1
BIGRAG_CHAT_API_KEY: e2e-fake-key
BIGRAG_ALLOW_PRIVATE_CHAT_BASE_URLS: "true"
- BIGRAG_TURBOPUFFER_API_KEY: e2e-fake-turbopuffer-key
- BIGRAG_TURBOPUFFER_REGION: e2e
- BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: e2e_
- BIGRAG_TURBOPUFFER_BASE_URL: http://fake-turbopuffer:9002
BIGRAG_ALLOW_LOCAL_WEBHOOKS: "true"
BIGRAG_BACKUP_S3_BUCKET: bigrag-backups
BIGRAG_BACKUP_S3_ENDPOINT_URL: http://minio:9000
@@ -68,10 +64,6 @@ services:
BIGRAG_CHAT_BASE_URL: http://fake-openai:9001/v1
BIGRAG_CHAT_API_KEY: e2e-fake-key
BIGRAG_ALLOW_PRIVATE_CHAT_BASE_URLS: "true"
- BIGRAG_TURBOPUFFER_API_KEY: e2e-fake-turbopuffer-key
- BIGRAG_TURBOPUFFER_REGION: e2e
- BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: e2e_
- BIGRAG_TURBOPUFFER_BASE_URL: http://fake-turbopuffer:9002
BIGRAG_ALLOW_LOCAL_WEBHOOKS: "true"
depends_on:
bigrag-api:
diff --git a/website/content/docs/deployment/docker.mdx b/website/content/docs/deployment/docker.mdx
index 293d43b8..726b2fd8 100644
--- a/website/content/docs/deployment/docker.mdx
+++ b/website/content/docs/deployment/docker.mdx
@@ -40,7 +40,7 @@ curl http://localhost:4000/health
## Docker Compose
-The default `docker-compose.yml` runs the API, worker, admin UI, Postgres, and Redis. Turbopuffer is configured through environment variables and does not run as a local Compose service. When hacking from a checkout, plain `docker compose up -d` builds local API and UI images. When deploying published images, set the image variables shown above and use `--no-build`.
+The default `docker-compose.yml` runs the API, worker, admin UI, Postgres, and Redis. Turbopuffer is configured from the admin UI and stored in Postgres; it does not run as a local Compose service. When hacking from a checkout, plain `docker compose up -d` builds local API and UI images. When deploying published images, set the image variables shown above and use `--no-build`.
```yaml
services:
@@ -53,10 +53,6 @@ services:
environment:
BIGRAG_ENV: "${BIGRAG_ENV:-dev}"
BIGRAG_DATABASE_URL: postgres://bigrag:bigrag@postgres:5432/bigrag?sslmode=disable
- BIGRAG_TURBOPUFFER_API_KEY: "${BIGRAG_TURBOPUFFER_API_KEY:-}"
- BIGRAG_TURBOPUFFER_REGION: "${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}"
- BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: "${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}"
- BIGRAG_TURBOPUFFER_BASE_URL: "${BIGRAG_TURBOPUFFER_BASE_URL:-}"
BIGRAG_REDIS_URL: "${BIGRAG_REDIS_URL:-redis://redis:6379/0}"
BIGRAG_MASTER_KEY: "${BIGRAG_MASTER_KEY:-}"
BIGRAG_MASTER_KEY_PREVIOUS: '${BIGRAG_MASTER_KEY_PREVIOUS:-[]}'
@@ -87,10 +83,6 @@ services:
environment:
BIGRAG_ENV: "${BIGRAG_ENV:-dev}"
BIGRAG_DATABASE_URL: postgres://bigrag:bigrag@postgres:5432/bigrag?sslmode=disable
- BIGRAG_TURBOPUFFER_API_KEY: "${BIGRAG_TURBOPUFFER_API_KEY:-}"
- BIGRAG_TURBOPUFFER_REGION: "${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}"
- BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: "${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}"
- BIGRAG_TURBOPUFFER_BASE_URL: "${BIGRAG_TURBOPUFFER_BASE_URL:-}"
BIGRAG_REDIS_URL: "${BIGRAG_REDIS_URL:-redis://redis:6379/0}"
BIGRAG_UPLOAD_DIR: /data/uploads
BIGRAG_MASTER_KEY: "${BIGRAG_MASTER_KEY:-}"
diff --git a/website/content/docs/deployment/encryption.mdx b/website/content/docs/deployment/encryption.mdx
index c54dc3c4..439139f9 100644
--- a/website/content/docs/deployment/encryption.mdx
+++ b/website/content/docs/deployment/encryption.mdx
@@ -60,7 +60,7 @@ bigRAG delegates the bulk of at-rest protection to your infrastructure. For a pr
### Turbopuffer
- Turbopuffer stores vectors, full-text fields, payload attributes, and indexes in its managed service. Select the production region deliberately and keep the API key scoped to the bigRAG service.
-- Store `BIGRAG_TURBOPUFFER_API_KEY` in your secrets manager, rotate it on the same cadence as other provider credentials, and restrict outbound network access where your platform allows it.
+- Save the Turbopuffer API key in the admin UI so it is encrypted in the `instance_settings` table with `BIGRAG_MASTER_KEY`. Rotate it on the same cadence as other provider credentials, and restrict outbound network access where your platform allows it.
### Redis
diff --git a/website/content/docs/deployment/production.mdx b/website/content/docs/deployment/production.mdx
index 36e53277..c9c97ec5 100644
--- a/website/content/docs/deployment/production.mdx
+++ b/website/content/docs/deployment/production.mdx
@@ -22,7 +22,7 @@ Tick these off before pointing real traffic at a bigRAG deployment:
proxies if bigRAG sits behind nginx, Caddy, Traefik, or a load balancer.
- [ ] Redis with `requirepass` set and `appendonly yes` persisted to
a mounted volume.
-- [ ] Turbopuffer API key and region configured for managed vector and full-text search.
+- [ ] Turbopuffer API key and region saved from the admin UI for managed vector and full-text search.
- [ ] Postgres warm-standby replica for failover.
- [ ] Backup strategy: configure readable S3/R2 backups from `/backups`,
protect that bucket as sensitive, and separately keep encrypted infrastructure
@@ -62,10 +62,6 @@ services:
environment:
BIGRAG_ENV: prod
BIGRAG_DATABASE_URL: postgres://bigrag:strongpassword@postgres:5432/bigrag
- BIGRAG_TURBOPUFFER_API_KEY: ${BIGRAG_TURBOPUFFER_API_KEY}
- BIGRAG_TURBOPUFFER_REGION: ${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}
- BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: ${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}
- BIGRAG_TURBOPUFFER_BASE_URL: ${BIGRAG_TURBOPUFFER_BASE_URL:-}
BIGRAG_REDIS_URL: ${BIGRAG_REDIS_URL}
BIGRAG_HOST: 0.0.0.0
BIGRAG_MASTER_KEY: ${BIGRAG_MASTER_KEY}
@@ -103,10 +99,6 @@ services:
environment:
BIGRAG_ENV: prod
BIGRAG_DATABASE_URL: postgres://bigrag:strongpassword@postgres:5432/bigrag
- BIGRAG_TURBOPUFFER_API_KEY: ${BIGRAG_TURBOPUFFER_API_KEY}
- BIGRAG_TURBOPUFFER_REGION: ${BIGRAG_TURBOPUFFER_REGION:-aws-us-east-1}
- BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX: ${BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX:-bigrag_}
- BIGRAG_TURBOPUFFER_BASE_URL: ${BIGRAG_TURBOPUFFER_BASE_URL:-}
BIGRAG_REDIS_URL: ${BIGRAG_REDIS_URL}
BIGRAG_MASTER_KEY: ${BIGRAG_MASTER_KEY}
BIGRAG_MASTER_KEY_PREVIOUS: '${BIGRAG_MASTER_KEY_PREVIOUS:-[]}'
@@ -219,9 +211,6 @@ REDIS_PASSWORD=
BIGRAG_REDIS_URL=redis://:@redis:6379/0
BIGRAG_MASTER_KEY=
BIGRAG_ALLOW_PUBLIC_BIND_IN_PROD=true # required when the API image binds 0.0.0.0 in prod
-BIGRAG_TURBOPUFFER_API_KEY=
-BIGRAG_TURBOPUFFER_REGION=aws-us-east-1
-BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX=bigrag_
# Performance
BIGRAG_WORKERS=8 # Match CPU cores
@@ -342,7 +331,7 @@ File exceeds the max upload size. Increase `BIGRAG_MAX_UPLOAD_SIZE_MB`.
### `/health/ready` returns 503 but services are running
-The readiness endpoint also checks the embedding provider. If no collection has an `embedding_api_key` configured, it reports degraded status. Check `/health` first to confirm the server itself is running, then create a collection with a valid embedding key.
+The readiness endpoint also checks Turbopuffer and the embedding provider. Check `/health` first to confirm the server itself is running, then save Turbopuffer settings in the admin UI and create a collection with a valid embedding key.
### Document uploads fail with embedding errors
diff --git a/website/content/docs/deployment/railway.mdx b/website/content/docs/deployment/railway.mdx
index 8e11fc87..86dc59c1 100644
--- a/website/content/docs/deployment/railway.mdx
+++ b/website/content/docs/deployment/railway.mdx
@@ -14,7 +14,7 @@ This guide deploys the API, Dramatiq worker, admin UI, Postgres, and Redis in on
- **App** — admin UI, public domain.
- **Postgres 17** — Railway plugin, exposes `DATABASE_URL`.
- **Redis 7** — Railway plugin, exposes `REDIS_URL`.
-- **Turbopuffer** — managed vector and full-text search, configured with environment variables.
+- **Turbopuffer** — managed vector and full-text search, configured from the admin UI.
## Prerequisites
@@ -68,9 +68,6 @@ BIGRAG_LOG_LEVEL=info
BIGRAG_LOG_FORMAT=json
BIGRAG_DATABASE_URL=${{Postgres.DATABASE_URL}}
BIGRAG_REDIS_URL=${{Redis.REDIS_URL}}
-BIGRAG_TURBOPUFFER_API_KEY=
-BIGRAG_TURBOPUFFER_REGION=aws-us-east-1
-BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX=bigrag_
BIGRAG_EMBEDDING_API_KEY=
BIGRAG_MASTER_KEY=
BIGRAG_ALLOW_PUBLIC_BIND_IN_PROD=true
@@ -80,7 +77,7 @@ BIGRAG_CORS_ORIGINS=["https://${{App.RAILWAY_PUBLIC_DOMAIN}}"]
BIGRAG_UPLOAD_DIR=/data/uploads
```
-Use the Turbopuffer region closest to your Railway services and embedding provider.
+Save the Turbopuffer API key and region from the admin UI after setup.
## Worker
@@ -93,7 +90,7 @@ Use the Turbopuffer region closest to your Railway services and embedding provid
| Volume | same mounted upload volume path as API |
| Public domain | no |
-Use the same environment as `API`, including `BIGRAG_DATABASE_URL`, `BIGRAG_REDIS_URL`, `BIGRAG_TURBOPUFFER_API_KEY`, `BIGRAG_TURBOPUFFER_REGION`, `BIGRAG_MASTER_KEY`, and `BIGRAG_UPLOAD_DIR`.
+Use the same environment as `API`, including `BIGRAG_DATABASE_URL`, `BIGRAG_REDIS_URL`, `BIGRAG_MASTER_KEY`, and `BIGRAG_UPLOAD_DIR`.
## Generate the master key
@@ -110,12 +107,13 @@ Back the master key up immediately. It encrypts provider secrets, webhook secret
1. Wait for Postgres, Redis, API, Worker, and app services to deploy.
2. Open the admin UI public URL.
3. Complete `/setup` to create the first admin account.
-4. Create a collection and upload a small document.
-5. Check `/health/ready`; it should report `vector_store: true`.
+4. Save the Turbopuffer API key and region in the admin UI.
+5. Create a collection and upload a small document.
+6. Check `/health/ready`; it should report `vector_store: true`.
## Troubleshooting
-**`/health/ready` reports a degraded vector store.** Check `BIGRAG_TURBOPUFFER_API_KEY`, `BIGRAG_TURBOPUFFER_REGION`, and any outbound network restrictions, then redeploy the API and Worker services.
+**`/health/ready` reports a degraded vector store.** Check the Turbopuffer settings in the admin UI and any outbound network restrictions.
**API exits with `ImportError: Can't find Python file .../site-packages/alembic/env.py`.** Rebuild from the latest repo revision and clear Railway's build cache if it keeps reusing an old image. The API image bundles bigRAG's Alembic migration environment into the installed package; this error means the deployed image was built without that migration environment.
diff --git a/website/content/docs/getting-started/configuration.mdx b/website/content/docs/getting-started/configuration.mdx
index e73f630a..f1e09065 100644
--- a/website/content/docs/getting-started/configuration.mdx
+++ b/website/content/docs/getting-started/configuration.mdx
@@ -26,10 +26,6 @@ Bootstrap config is the small set of values the API needs before it can connect
| `BIGRAG_DB_POOL_MAX` | Maximum Postgres pool size. | `50` |
| `BIGRAG_MIGRATION_TIMEOUT_SECONDS` | Startup migration timeout. Set `0` to disable the timeout. | `60` |
| `BIGRAG_REDIS_URL` | Redis connection URL. | `redis://localhost:6379/0` |
-| `BIGRAG_TURBOPUFFER_API_KEY` | Turbopuffer API key for vector search. | — |
-| `BIGRAG_TURBOPUFFER_REGION` | Turbopuffer region. | `aws-us-east-1` |
-| `BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX` | Prefix prepended to generated Turbopuffer namespace names. | `bigrag_` |
-| `BIGRAG_TURBOPUFFER_BASE_URL` | Optional Turbopuffer-compatible endpoint for local/e2e services. | — |
| `BIGRAG_MASTER_KEY` | Fernet key for encrypted provider secrets, instance-setting secrets, embedding-cache rows, and Redis cache payloads. Required for production. | — |
| `BIGRAG_MASTER_KEY_PREVIOUS` | JSON array of old Fernet keys during staged key rotation. | `[]` |
| `BIGRAG_UPLOAD_DIR` | Local document upload directory when storage backend is `local`. | `./data/uploads` |
@@ -56,9 +52,6 @@ workers = 1
database_url = "postgres://bigrag:change-me@postgres:5432/bigrag?sslmode=disable"
redis_url = "redis://redis:6379/0"
-turbopuffer_api_key = "tpuf_..."
-turbopuffer_region = "aws-us-east-1"
-turbopuffer_namespace_prefix = "bigrag_"
master_key = "generate-a-fernet-key"
master_key_previous = []
@@ -70,7 +63,7 @@ Environment variables override TOML values. CLI flags passed to `python -m bigra
## Runtime settings
-After the first admin account exists, go to `/settings` to manage platform runtime settings, `/data-storage` to manage uploaded source-file storage, and `/models` to manage model presets and model runtime settings. Turbopuffer is the vector backend for every collection. Runtime settings are stored in Postgres in the `instance_settings` table. Secret settings are encrypted with `BIGRAG_MASTER_KEY` and redacted on read.
+After the first admin account exists, go to `/settings` to manage platform runtime settings, `/data-storage` to manage uploaded source-file storage, and `/models` to manage model presets and model runtime settings. Turbopuffer is configured from the UI, is the vector backend for every collection, and is stored in Postgres in the `instance_settings` table. Secret settings are encrypted with `BIGRAG_MASTER_KEY` and redacted on read.
The same surface is available through [`/v1/admin/settings`](/docs/api-reference/instance-settings).
@@ -90,7 +83,7 @@ The UI uses the same compact top tab control and shared dashboard body width as
Database-backed admin settings apply immediately on save. Storage, backup, and Turbopuffer connection changes validate the new target before saving, then swap the runtime client in place where applicable.
-Legacy runtime environment variables are still read as initial defaults for compatibility, but enterprise installs should treat `/settings` as the control plane after bootstrap.
+Turbopuffer settings are not bootstrap environment variables. Save them from the admin UI so the API and worker read the same database-backed instance settings.
## CLI flags
From c367746b983b79747bba68704322a22c3b3603d0 Mon Sep 17 00:00:00 2001
From: Bigint <69431456+bigint@users.noreply.github.com>
Date: Thu, 21 May 2026 23:41:01 +0530
Subject: [PATCH 8/9] feat: add first-run provider onboarding
---
.../features/onboarding/onboarding-page.tsx | 176 ++++++++++++++++++
.../onboarding/onboarding-progress.tsx | 47 +++++
.../features/onboarding/onboarding-state.ts | 55 ++++++
.../onboarding/onboarding-step-panel.tsx | 85 +++++++++
.../features/onboarding/turbopuffer-form.tsx | 92 +++++++++
app/src/routeTree.gen.ts | 21 +++
app/src/routes/_auth.setup.tsx | 2 +-
app/src/routes/_dashboard.onboarding.tsx | 6 +
website/content/docs/admin-ui.mdx | 3 +-
.../docs/api-reference/authentication.mdx | 2 +-
.../docs/getting-started/configuration.mdx | 2 +-
.../docs/getting-started/quickstart.mdx | 8 +-
12 files changed, 494 insertions(+), 5 deletions(-)
create mode 100644 app/src/features/onboarding/onboarding-page.tsx
create mode 100644 app/src/features/onboarding/onboarding-progress.tsx
create mode 100644 app/src/features/onboarding/onboarding-state.ts
create mode 100644 app/src/features/onboarding/onboarding-step-panel.tsx
create mode 100644 app/src/features/onboarding/turbopuffer-form.tsx
create mode 100644 app/src/routes/_dashboard.onboarding.tsx
diff --git a/app/src/features/onboarding/onboarding-page.tsx b/app/src/features/onboarding/onboarding-page.tsx
new file mode 100644
index 00000000..9704c86d
--- /dev/null
+++ b/app/src/features/onboarding/onboarding-page.tsx
@@ -0,0 +1,176 @@
+import { useNavigate } from "@tanstack/react-router";
+import { CheckCircle2, Cpu, Database, Plus, TriangleAlert } from "lucide-react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Page } from "@/components/ui/page";
+import { Spinner } from "@/components/ui/spinner";
+import { PresetForm } from "@/features/models/preset-form";
+import { ProgressPanel } from "@/features/onboarding/onboarding-progress";
+import {
+ canSaveTurbopufferDraft,
+ hasTurbopufferApiKey,
+ type TurbopufferDraft,
+ turbopufferDraftFromSettings,
+ turbopufferSettingsBody,
+} from "@/features/onboarding/onboarding-state";
+import { PresetSummary, StepPanel } from "@/features/onboarding/onboarding-step-panel";
+import { TurbopufferForm } from "@/features/onboarding/turbopuffer-form";
+import { useEmbeddingPresets } from "@/hooks/use-embedding-presets";
+import { useInstanceSettings, useUpdateInstanceSettings } from "@/hooks/use-instance-settings";
+
+export const OnboardingPage = () => {
+ const navigate = useNavigate();
+ const presets = useEmbeddingPresets();
+ const settings = useInstanceSettings();
+ const saveSettings = useUpdateInstanceSettings();
+ const [presetOpen, setPresetOpen] = useState(false);
+ const [turbopufferSkipped, setTurbopufferSkipped] = useState(false);
+ const [draft, setDraft] = useState(() =>
+ turbopufferDraftFromSettings(undefined),
+ );
+
+ useEffect(() => {
+ if (settings.data) setDraft(turbopufferDraftFromSettings(settings.data));
+ }, [settings.data]);
+
+ const presetList = presets.data?.presets ?? [];
+ const firstPreset = presetList[0];
+ const embeddingComplete = presetList.length > 0;
+ const turbopufferComplete = hasTurbopufferApiKey(settings.data);
+ const canSaveTurbopuffer = canSaveTurbopufferDraft(draft, turbopufferComplete);
+ const loading = presets.isPending || settings.isPending;
+ const error = presets.error ?? settings.error;
+
+ const saveTurbopuffer = async () => {
+ if (!canSaveTurbopuffer) {
+ toast.error("Add a Turbopuffer API key or skip this step");
+ return;
+ }
+ try {
+ await saveSettings.mutateAsync({
+ values: turbopufferSettingsBody(draft, turbopufferComplete),
+ });
+ setTurbopufferSkipped(false);
+ } catch {}
+ };
+
+ const finish = () => {
+ if (!embeddingComplete) return;
+ navigate({ to: "/overview", replace: true });
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+
+
Onboarding unavailable
+
+ {error instanceof Error ? error.message : "Could not load setup state."}
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Finish setup
+
+ }
+ description="Connect the provider pieces bigRAG needs before indexing documents."
+ eyebrow={First-run onboarding}
+ title="Connect providers"
+ />
+
+
+
+
+ {embeddingComplete && firstPreset ? (
+
+ ) : (
+
+
+ Create one verified embedding preset. Collections can reuse it when they are
+ created.
+
+
+
+
+
+ )}
+
+
+
+ {
+ setTurbopufferSkipped(true);
+ toast.message("Turbopuffer skipped for now");
+ }}
+ pending={saveSettings.isPending}
+ saveDisabled={!canSaveTurbopuffer}
+ skipped={turbopufferSkipped}
+ />
+
+
+
+
+
+
+ setPresetOpen(false)} open={presetOpen} />
+
+ );
+};
diff --git a/app/src/features/onboarding/onboarding-progress.tsx b/app/src/features/onboarding/onboarding-progress.tsx
new file mode 100644
index 00000000..f32dfff5
--- /dev/null
+++ b/app/src/features/onboarding/onboarding-progress.tsx
@@ -0,0 +1,47 @@
+import { CheckCircle2 } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+
+export const ProgressPanel = ({
+ embeddingComplete,
+ onFinish,
+ turbopufferComplete,
+ turbopufferSkipped,
+}: {
+ readonly embeddingComplete: boolean;
+ readonly onFinish: () => void;
+ readonly turbopufferComplete: boolean;
+ readonly turbopufferSkipped: boolean;
+}) => (
+
+);
+
+const ProgressRow = ({
+ complete,
+ label,
+ required = false,
+}: {
+ readonly complete: boolean;
+ readonly label: string;
+ readonly required?: boolean;
+}) => (
+
+ {label}
+
+ {complete ? "done" : required ? "required" : "optional"}
+
+
+);
diff --git a/app/src/features/onboarding/onboarding-state.ts b/app/src/features/onboarding/onboarding-state.ts
new file mode 100644
index 00000000..3be794cb
--- /dev/null
+++ b/app/src/features/onboarding/onboarding-state.ts
@@ -0,0 +1,55 @@
+import type { InstanceSettingsResponse } from "@/types/bigrag";
+
+export type TurbopufferDraft = {
+ apiKey: string;
+ baseUrl: string;
+ namespacePrefix: string;
+ region: string;
+};
+
+export const DEFAULT_TURBOPUFFER_REGION = "aws-us-east-1";
+export const DEFAULT_TURBOPUFFER_NAMESPACE_PREFIX = "bigrag_";
+
+const settingString = (
+ settings: InstanceSettingsResponse | undefined,
+ key: string,
+ fallback: string,
+) => {
+ const value = settings?.values[key]?.value;
+ return typeof value === "string" ? value : fallback;
+};
+
+export const turbopufferDraftFromSettings = (
+ settings: InstanceSettingsResponse | undefined,
+): TurbopufferDraft => ({
+ apiKey: "",
+ baseUrl: settingString(settings, "turbopuffer_base_url", ""),
+ namespacePrefix: settingString(
+ settings,
+ "turbopuffer_namespace_prefix",
+ DEFAULT_TURBOPUFFER_NAMESPACE_PREFIX,
+ ),
+ region: settingString(settings, "turbopuffer_region", DEFAULT_TURBOPUFFER_REGION),
+});
+
+export const hasTurbopufferApiKey = (settings: InstanceSettingsResponse | undefined) =>
+ Boolean(settings?.values.turbopuffer_api_key?.has_value);
+
+export const canSaveTurbopufferDraft = (draft: TurbopufferDraft, hasSavedKey: boolean) =>
+ hasSavedKey || Boolean(draft.apiKey.trim());
+
+export const turbopufferSettingsBody = (
+ draft: TurbopufferDraft,
+ hasSavedKey: boolean,
+): Record => {
+ const values: Record = {
+ turbopuffer_base_url: draft.baseUrl.trim() || null,
+ turbopuffer_namespace_prefix:
+ draft.namespacePrefix.trim() || DEFAULT_TURBOPUFFER_NAMESPACE_PREFIX,
+ turbopuffer_region: draft.region.trim() || DEFAULT_TURBOPUFFER_REGION,
+ };
+ if (!hasSavedKey || draft.apiKey.trim()) {
+ values.turbopuffer_api_key = draft.apiKey.trim();
+ }
+ return values;
+};
diff --git a/app/src/features/onboarding/onboarding-step-panel.tsx b/app/src/features/onboarding/onboarding-step-panel.tsx
new file mode 100644
index 00000000..74d9444d
--- /dev/null
+++ b/app/src/features/onboarding/onboarding-step-panel.tsx
@@ -0,0 +1,85 @@
+import { CheckCircle2, type LucideIcon } from "lucide-react";
+import type { ReactNode } from "react";
+import { Badge } from "@/components/ui/badge";
+import { cn } from "@/lib/cn";
+import type { EmbeddingPreset } from "@/types/bigrag";
+
+type StepPanelProps = {
+ readonly active: boolean;
+ readonly children: ReactNode;
+ readonly complete: boolean;
+ readonly icon: LucideIcon;
+ readonly index: number;
+ readonly optional?: boolean;
+ readonly title: string;
+};
+
+export const StepPanel = ({
+ active,
+ children,
+ complete,
+ icon: Icon,
+ index,
+ optional = false,
+ title,
+}: StepPanelProps) => (
+
+);
+
+export const PresetSummary = ({
+ preset,
+ total,
+}: {
+ readonly preset: EmbeddingPreset;
+ readonly total: number;
+}) => (
+
+
+
{preset.name}
+
+ {preset.provider}
+ {preset.model}
+ {preset.dimension}d
+
+
+
+
+ {preset.has_api_key ? "key set" : "key missing"}
+
+ {total > 1 && {total} presets}
+
+
+);
diff --git a/app/src/features/onboarding/turbopuffer-form.tsx b/app/src/features/onboarding/turbopuffer-form.tsx
new file mode 100644
index 00000000..0af7f9db
--- /dev/null
+++ b/app/src/features/onboarding/turbopuffer-form.tsx
@@ -0,0 +1,92 @@
+import { ArrowRight, Save } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Spinner } from "@/components/ui/spinner";
+import type { TurbopufferDraft } from "@/features/onboarding/onboarding-state";
+
+type TurbopufferFormProps = {
+ readonly complete: boolean;
+ readonly draft: TurbopufferDraft;
+ readonly onDraftChange: (draft: TurbopufferDraft) => void;
+ readonly onSave: () => void;
+ readonly onSkip: () => void;
+ readonly pending: boolean;
+ readonly saveDisabled: boolean;
+ readonly skipped: boolean;
+};
+
+export const TurbopufferForm = ({
+ complete,
+ draft,
+ onDraftChange,
+ onSave,
+ onSkip,
+ pending,
+ saveDisabled,
+ skipped,
+}: TurbopufferFormProps) => {
+ const patchDraft = (patch: Partial) => onDraftChange({ ...draft, ...patch });
+ return (
+
+ );
+};
diff --git a/app/src/routeTree.gen.ts b/app/src/routeTree.gen.ts
index 2908d41d..d755d516 100644
--- a/app/src/routeTree.gen.ts
+++ b/app/src/routeTree.gen.ts
@@ -12,6 +12,7 @@ import { Route as DashboardWebhooksRouteImport } from "./routes/_dashboard.webho
import { Route as DashboardUsageRouteImport } from "./routes/_dashboard.usage";
import { Route as DashboardSettingsRouteImport } from "./routes/_dashboard.settings";
import { Route as DashboardOverviewRouteImport } from "./routes/_dashboard.overview";
+import { Route as DashboardOnboardingRouteImport } from "./routes/_dashboard.onboarding";
import { Route as DashboardModelsRouteImport } from "./routes/_dashboard.models";
import { Route as DashboardMcpRouteImport } from "./routes/_dashboard.mcp";
import { Route as DashboardEvalsRouteImport } from "./routes/_dashboard.evals";
@@ -68,6 +69,11 @@ const DashboardOverviewRoute = DashboardOverviewRouteImport.update({
path: "/overview",
getParentRoute: () => DashboardRoute,
} as any);
+const DashboardOnboardingRoute = DashboardOnboardingRouteImport.update({
+ id: "/onboarding",
+ path: "/onboarding",
+ getParentRoute: () => DashboardRoute,
+} as any);
const DashboardModelsRoute = DashboardModelsRouteImport.update({
id: "/models",
path: "/models",
@@ -203,6 +209,7 @@ export interface FileRoutesByFullPath {
"/evals": typeof DashboardEvalsRoute;
"/mcp": typeof DashboardMcpRoute;
"/models": typeof DashboardModelsRoute;
+ "/onboarding": typeof DashboardOnboardingRoute;
"/overview": typeof DashboardOverviewRoute;
"/settings": typeof DashboardSettingsRoute;
"/usage": typeof DashboardUsageRoute;
@@ -232,6 +239,7 @@ export interface FileRoutesByTo {
"/evals": typeof DashboardEvalsRoute;
"/mcp": typeof DashboardMcpRoute;
"/models": typeof DashboardModelsRoute;
+ "/onboarding": typeof DashboardOnboardingRoute;
"/overview": typeof DashboardOverviewRoute;
"/settings": typeof DashboardSettingsRoute;
"/usage": typeof DashboardUsageRoute;
@@ -262,6 +270,7 @@ export interface FileRoutesById {
"/_dashboard/evals": typeof DashboardEvalsRoute;
"/_dashboard/mcp": typeof DashboardMcpRoute;
"/_dashboard/models": typeof DashboardModelsRoute;
+ "/_dashboard/onboarding": typeof DashboardOnboardingRoute;
"/_dashboard/overview": typeof DashboardOverviewRoute;
"/_dashboard/settings": typeof DashboardSettingsRoute;
"/_dashboard/usage": typeof DashboardUsageRoute;
@@ -293,6 +302,7 @@ export interface FileRouteTypes {
| "/evals"
| "/mcp"
| "/models"
+ | "/onboarding"
| "/overview"
| "/settings"
| "/usage"
@@ -322,6 +332,7 @@ export interface FileRouteTypes {
| "/evals"
| "/mcp"
| "/models"
+ | "/onboarding"
| "/overview"
| "/settings"
| "/usage"
@@ -351,6 +362,7 @@ export interface FileRouteTypes {
| "/_dashboard/evals"
| "/_dashboard/mcp"
| "/_dashboard/models"
+ | "/_dashboard/onboarding"
| "/_dashboard/overview"
| "/_dashboard/settings"
| "/_dashboard/usage"
@@ -424,6 +436,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof DashboardOverviewRouteImport;
parentRoute: typeof DashboardRoute;
};
+ "/_dashboard/onboarding": {
+ id: "/_dashboard/onboarding";
+ path: "/onboarding";
+ fullPath: "/onboarding";
+ preLoaderRoute: typeof DashboardOnboardingRouteImport;
+ parentRoute: typeof DashboardRoute;
+ };
"/_dashboard/models": {
id: "/_dashboard/models";
path: "/models";
@@ -650,6 +669,7 @@ interface DashboardRouteChildren {
DashboardEvalsRoute: typeof DashboardEvalsRoute;
DashboardMcpRoute: typeof DashboardMcpRoute;
DashboardModelsRoute: typeof DashboardModelsRoute;
+ DashboardOnboardingRoute: typeof DashboardOnboardingRoute;
DashboardOverviewRoute: typeof DashboardOverviewRoute;
DashboardSettingsRoute: typeof DashboardSettingsRoute;
DashboardUsageRoute: typeof DashboardUsageRoute;
@@ -669,6 +689,7 @@ const DashboardRouteChildren: DashboardRouteChildren = {
DashboardEvalsRoute: DashboardEvalsRoute,
DashboardMcpRoute: DashboardMcpRoute,
DashboardModelsRoute: DashboardModelsRoute,
+ DashboardOnboardingRoute: DashboardOnboardingRoute,
DashboardOverviewRoute: DashboardOverviewRoute,
DashboardSettingsRoute: DashboardSettingsRoute,
DashboardUsageRoute: DashboardUsageRoute,
diff --git a/app/src/routes/_auth.setup.tsx b/app/src/routes/_auth.setup.tsx
index 4c4bbf46..88f38e07 100644
--- a/app/src/routes/_auth.setup.tsx
+++ b/app/src/routes/_auth.setup.tsx
@@ -30,7 +30,7 @@ const SetupPage = () => {
try {
await setup.mutateAsync(setupBodyFromValues(value));
toast.success("Admin account created");
- navigate({ to: "/overview", replace: true });
+ navigate({ to: "/onboarding", replace: true });
} catch (err) {
toast.error(err instanceof Error ? err.message : "Setup failed");
}
diff --git a/app/src/routes/_dashboard.onboarding.tsx b/app/src/routes/_dashboard.onboarding.tsx
new file mode 100644
index 00000000..cfcb9af1
--- /dev/null
+++ b/app/src/routes/_dashboard.onboarding.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { OnboardingPage } from "@/features/onboarding/onboarding-page";
+
+export const Route = createFileRoute("/_dashboard/onboarding")({
+ component: () => ,
+});
diff --git a/website/content/docs/admin-ui.mdx b/website/content/docs/admin-ui.mdx
index 6ba70afd..93ddf89a 100644
--- a/website/content/docs/admin-ui.mdx
+++ b/website/content/docs/admin-ui.mdx
@@ -30,6 +30,7 @@ For cross-site production deployments, enable secure session cookies and usually
| Path | Purpose |
|------|---------|
| `/setup` | One-time page to create the first admin account. Disappears once `needs_setup: false`. |
+| `/onboarding` | Authenticated first-run provider setup. Requires one verified embedding preset, prompts for Turbopuffer, and then continues to `/overview`. |
| `/login` | Email + password login for admins and members. |
| `/overview` | Platform-wide stats — collection count, document states, stored token count, queue depth, readiness, and worker heartbeat. |
| `/collections` | List, create, delete, and search collections. |
@@ -123,6 +124,6 @@ The admin UI uses session cookies exclusively. To automate anything the admin UI
## When to skip the Admin UI
-- **Pure backend deployment** — no admin UI process, everything via API keys and SDKs. Set `BIGRAG_ENV=prod` and use [`POST /v1/auth/setup`](/docs/api-reference/authentication) from an admin workstation to bootstrap.
+- **Pure backend deployment** — no admin UI process, everything via API keys and SDKs. Set `BIGRAG_ENV=prod`, use [`POST /v1/auth/setup`](/docs/api-reference/authentication) from an admin workstation to bootstrap, then create at least one embedding preset through the admin API before indexing documents.
- **Embedded experiences** — if you're surfacing search inside your own app, skip admin UI and build against the [query API](/docs/api-reference/query).
- **Read-only operators** — create a `member` account; they get `/overview`, `/collections` (read), and `/chat` but can't mutate.
diff --git a/website/content/docs/api-reference/authentication.mdx b/website/content/docs/api-reference/authentication.mdx
index 4f0e95b4..c70bc6fe 100644
--- a/website/content/docs/api-reference/authentication.mdx
+++ b/website/content/docs/api-reference/authentication.mdx
@@ -32,7 +32,7 @@ curl -X POST http://localhost:4000/v1/auth/setup \
Once at least one admin exists, `setup-status` returns `{"needs_setup": false}` and `/v1/auth/setup` returns `409`.
-The admin UI exposes the same flow at `http://localhost:3000/setup`.
+The admin UI exposes the same flow at `http://localhost:3000/setup`. After the first admin is created, the UI redirects the signed-in admin to `/onboarding` to create a verified embedding preset and optionally save Turbopuffer settings before continuing to `/overview`. The setup endpoint itself still only creates the first admin and session.
## Session auth (admin UI, browser)
diff --git a/website/content/docs/getting-started/configuration.mdx b/website/content/docs/getting-started/configuration.mdx
index f1e09065..6a906ea6 100644
--- a/website/content/docs/getting-started/configuration.mdx
+++ b/website/content/docs/getting-started/configuration.mdx
@@ -63,7 +63,7 @@ Environment variables override TOML values. CLI flags passed to `python -m bigra
## Runtime settings
-After the first admin account exists, go to `/settings` to manage platform runtime settings, `/data-storage` to manage uploaded source-file storage, and `/models` to manage model presets and model runtime settings. Turbopuffer is configured from the UI, is the vector backend for every collection, and is stored in Postgres in the `instance_settings` table. Secret settings are encrypted with `BIGRAG_MASTER_KEY` and redacted on read.
+After the first admin account exists, the admin UI sends first-run operators to `/onboarding` to create one verified embedding preset and optionally save Turbopuffer settings. Later, go to `/settings` to manage platform runtime settings, `/data-storage` to manage uploaded source-file storage, and `/models` to manage model presets and model runtime settings. Turbopuffer is configured from the UI, is the vector backend for every collection, and is stored in Postgres in the `instance_settings` table. Secret settings are encrypted with `BIGRAG_MASTER_KEY` and redacted on read.
The same surface is available through [`/v1/admin/settings`](/docs/api-reference/instance-settings).
diff --git a/website/content/docs/getting-started/quickstart.mdx b/website/content/docs/getting-started/quickstart.mdx
index 97fef456..dbc9f72f 100644
--- a/website/content/docs/getting-started/quickstart.mdx
+++ b/website/content/docs/getting-started/quickstart.mdx
@@ -42,7 +42,13 @@ curl -X POST $BASE/v1/auth/setup \
-c cookies.txt
```
-The response sets a `bigrag_session` cookie. The admin UI exposes the same flow at `http://localhost:3000/setup`.
+The response sets a `bigrag_session` cookie. The admin UI exposes the same flow at `http://localhost:3000/setup`, then redirects to `/onboarding` where you create one verified embedding preset and can optionally save Turbopuffer settings before landing on `/overview`.
+
+
+
+### Complete provider onboarding
+
+In the admin UI, finish `/onboarding` by adding one embedding preset. Turbopuffer can be saved there too, or skipped and configured later from the runtime settings API.
From a7557b72d841987049900c78590c9c57baf94367 Mon Sep 17 00:00:00 2001
From: Bigint <69431456+bigint@users.noreply.github.com>
Date: Thu, 21 May 2026 23:55:04 +0530
Subject: [PATCH 9/9] fix: make e2e wait for api liveness
---
api/bigrag/services/maintenance.py | 1 -
e2e/Makefile | 18 +++++++++++++-----
e2e/README.md | 7 ++++++-
website/content/docs/development/testing.mdx | 2 +-
4 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/api/bigrag/services/maintenance.py b/api/bigrag/services/maintenance.py
index e5a57a58..08e6fb55 100644
--- a/api/bigrag/services/maintenance.py
+++ b/api/bigrag/services/maintenance.py
@@ -11,7 +11,6 @@
MAINTENANCE_LOCK_NAME = "maintenance"
BACKUP_LOCK_NAME = MAINTENANCE_LOCK_NAME
-VECTOR_MIGRATION_LOCK_NAME = MAINTENANCE_LOCK_NAME
class MaintenanceActiveError(RuntimeError):
diff --git a/e2e/Makefile b/e2e/Makefile
index 13320a15..a86036b5 100644
--- a/e2e/Makefile
+++ b/e2e/Makefile
@@ -3,6 +3,7 @@
E2E_PROJECT ?= bigrag-e2e
COMPOSE = docker compose -p $(E2E_PROJECT) -f ../docker-compose.yml -f docker-compose.e2e.yml
API_BASE ?= http://localhost:4000
+API_LIVENESS ?= $(API_BASE)/health
E2E_SERVICES = bigrag-api bigrag-worker fake-openai fake-turbopuffer webhook-sink minio
up:
@@ -10,14 +11,14 @@ up:
$(MAKE) wait-ready
wait-ready:
- @echo "Waiting for $(API_BASE)/health/ready ..."
+ @echo "Waiting for $(API_LIVENESS) ..."
@for i in $$(seq 1 60); do \
- if curl -fsS $(API_BASE)/health/ready >/dev/null 2>&1; then \
+ if curl -fsS $(API_LIVENESS) >/dev/null 2>&1; then \
echo "ready"; exit 0; \
fi; \
sleep 2; \
done; \
- echo "timed out waiting for /health/ready"; exit 1
+ echo "timed out waiting for $(API_LIVENESS)"; exit 1
down:
$(COMPOSE) down -v
@@ -41,13 +42,20 @@ test-sdk-ts:
test: test-api test-sdk-py test-sdk-ts
e2e:
- $(MAKE) up
+ @if ! $(MAKE) up; then \
+ echo "=== bigrag-api logs (last 1000 lines) ==="; \
+ $(COMPOSE) logs --tail=1000 bigrag-api || true; \
+ echo "=== bigrag-worker logs (last 1000 lines) ==="; \
+ $(COMPOSE) logs --tail=1000 bigrag-worker || true; \
+ $(COMPOSE) down -v || true; \
+ exit 1; \
+ fi
@if ! $(MAKE) test; then \
echo "=== bigrag-api logs (last 1000 lines) ==="; \
$(COMPOSE) logs --tail=1000 bigrag-api || true; \
echo "=== bigrag-worker logs (last 1000 lines) ==="; \
$(COMPOSE) logs --tail=1000 bigrag-worker || true; \
- $(MAKE) down; \
+ $(COMPOSE) down -v || true; \
exit 1; \
fi
$(MAKE) down
diff --git a/e2e/README.md b/e2e/README.md
index d20c3abf..13a4881c 100644
--- a/e2e/README.md
+++ b/e2e/README.md
@@ -31,7 +31,7 @@ make e2e # up, test, down
| Target | What it does |
|--------------------|--------------------------------------------------------------------|
-| `make up` | Brings up the API/SDK e2e compose stack and waits for `/health/ready` |
+| `make up` | Brings up the API/SDK e2e compose stack and waits for `/health` |
| `make down` | Tears down the stack and removes volumes |
| `make logs` | Tails compose logs |
| `make install` | `uv sync` + `pnpm install` |
@@ -45,6 +45,11 @@ The Makefile uses the `bigrag-e2e` Docker Compose project. Its `down` target
removes e2e volumes only and does not remove the default `bigrag` dev volumes
used by `./dev.sh`.
+`make up` waits for API liveness on `/health`. The pytest admin fixture then
+creates the first admin when needed and seeds e2e runtime settings for
+`fake-openai`, `fake-turbopuffer`, and `webhook-sink` before tests that need
+configured providers run.
+
## Architecture
```
diff --git a/website/content/docs/development/testing.mdx b/website/content/docs/development/testing.mdx
index fa3c3c3f..14cf850d 100644
--- a/website/content/docs/development/testing.mdx
+++ b/website/content/docs/development/testing.mdx
@@ -37,4 +37,4 @@ The E2E workflow uses the local fake OpenAI service only. There is no maintained
The `e2e/` workspace contains API pytest coverage, Python SDK contract tests, and TypeScript SDK Vitest coverage. It intentionally does not include Playwright or other UI browser tests.
-The local `e2e/Makefile` runs Docker Compose under the `bigrag-e2e` project name. `make down` removes that isolated e2e stack and its volumes without deleting the default `bigrag` dev volumes used by `./dev.sh`.
+The local `e2e/Makefile` runs Docker Compose under the `bigrag-e2e` project name. `make up` waits for API liveness on `/health`; pytest then creates the first admin when needed and seeds fake-provider runtime settings before provider-dependent tests run. `make down` removes that isolated e2e stack and its volumes without deleting the default `bigrag` dev volumes used by `./dev.sh`.