Skip to main content

Aetherfy Vectors Python SDK

PyPI version Python Support License: MIT Tests

Memory for AI agents — conversations, knowledge bases, scrape logs, trading histories. Named scopes you create and search, replicated across regions, with no infrastructure to run.

🚀 Key Features

  • 🧠 Memory Primitives: Namespace for any named scope, Thread for conversations — not a raw collection API
  • 🌍 Global Performance: Low latency from anywhere in the world
  • ⚡ Intelligent Caching: High cache hit rates for optimal performance
  • 🛡️ Zero DevOps: No infrastructure management or regional deployment needed
  • 🤝 Workspaces: every collection lives in one; inside a deployed agent the agent's own is used
  • 📊 Built-in Analytics: Real-time performance metrics and usage insights
  • 🔧 Auto-Failover: Intelligent routing and retry mechanisms
  • 🔐 Enterprise Security: API key authentication and audit logging
  • 🪜 Escape Hatch: AetherfyVectorsClient underneath, compatible with qdrant-client ≤1.15.1's core methods (see Migration Compatibility)

📦 Installation

pip install aetherfy-vectors

🏃‍♂️ Quick Start

Every memory lives in a scope you create by name. Two kinds: a namespace for anything with a name, a thread for a conversation. Bring your own embedding vectors — Aetherfy stores and searches them, it does not generate them.

from aetherfy_memory import MemoryClient

memory = MemoryClient()  # inside an Aetherfy agent: its key and its workspace

# --- A namespace: any named scope ---------------------------------------
memory.create_namespace("customer-42")
customer = memory.namespace("customer-42")

customer.add(text="Lives in NYC, prefers email", vector=embed("Lives in NYC, prefers email"))
hits = customer.search(vector=embed("where is this customer based?"), limit=5)

# --- A thread: a conversation -------------------------------------------
memory.create_thread("conv-99")
thread = memory.thread("conv-99")

thread.add(role="user", content="hi", vector=embed("hi"))
thread.add(role="assistant", content="hello", vector=embed("hello"))
recent = thread.history(limit=20)   # in message order

# Clearing a scope is atomic. After it, the scope no longer exists.
thread.clear()

Scopes must be created before you write to them, so a typo raises instead of silently creating a second store.

A namespace is one collection, and takes its own vector_size (default 384; pass 1536 for OpenAI small, 3072 for large, 1024 for Cohere v3).

Threads do not work that way. Every thread in a workspace lives in one shared collection with the thread id as a payload key, so a conversation costs no collection slot and a plan's collection limit does not cap how many conversations you can have. One collection means one dimension for all of them, so create_thread takes no vector_size: it comes from the client.

That collection is __threads__, one in each workspace (collection names are unique per workspace). It is managed by the SDK, and Aetherfy refuses to move or rename it: it holds the conversations of every agent in its workspace, so moving it would hand them to another workspace's agents. An agent that moves to another workspace leaves its conversation history behind in the old one.

memory = MemoryClient(thread_vector_size=1536)   # threads, for OpenAI small

Deployed on Aetherfy, MemoryClient() takes no arguments at all: the control plane injects AETHERFY_API_KEY and AETHERFY_WORKSPACE_ID (the agent's workspace) at deploy time. Anywhere else, name a workspace: MemoryClient(workspace="default").

Full memory API — iteration, bulk loading, metadata — under Memory SDK below.

🧱 The Low-Level Client

AetherfyVectorsClient is the layer MemoryClient is built on, exported and supported. Use it when you want raw collections and points rather than named scopes, or an operation the memory layer doesn't expose.

The snippets below build on each other; each assumes the imports and client from the previous ones.

Migration from qdrant-client

If your code uses qdrant-client 1.15.1 or earlier and sticks to the methods listed under Migration Compatibility, migrating means changing the import and the constructor call:

# Before (qdrant-client)
from qdrant_client import QdrantClient
client = QdrantClient(host="localhost", port=6333)

# After (aetherfy-vectors): the import and the constructor change
from aetherfy_vectors import AetherfyVectorsClient
client = AetherfyVectorsClient(api_key="afy_live_your_api_key_here", workspace="default")

# Calls to the listed methods stay as they are
results = client.search(collection_name="my_collection", query_vector=[0.1, 0.2, 0.3])

That holds when qdrant-client arguments are passed by keyword, and it has limits:

  • Some qdrant-client arguments are refused, because ignoring them would change what you get: ordering other than 'weak', shard_key_selector, consistency, search(append_payload=False), and every sharding, replication, storage and index setting of create_collection. Each raises TypeError naming itself.
  • Code using query_points (qdrant-client 1.16 and later, which removed search) does not migrate. This SDK has no query_points.
  • Methods not in the list (recommend, discover, the snapshot and alias APIs, and so on) do not exist here.

Basic Usage Example

from aetherfy_vectors import AetherfyVectorsClient
from aetherfy_vectors.models import VectorConfig, DistanceMetric

# Initialize client
client = AetherfyVectorsClient(api_key="afy_live_your_api_key_here", workspace="default")

# Create a collection
client.create_collection(
    "documents",
    VectorConfig(size=128, distance=DistanceMetric.COSINE)
)

# Add vectors — a point id is an unsigned integer (<= 2**53 - 1) or a UUID string
points = [
    {
        "id": 1,
        "vector": [0.1, 0.2, ...],  # 128-dimensional vector
        "payload": {"title": "Document 1", "category": "research"}
    }
]
client.upsert("documents", points)

# Search for similar vectors
results = client.search(
    collection_name="documents",
    query_vector=[0.1, 0.2, ...],
    limit=10,
    with_payload=True
)

for result in results:
    print(f"ID: {result.id}, Score: {result.score}")
    print(f"Title: {result.payload['title']}")

🌟 Unique Features

Usage Against Your Plan Limits

usage = client.get_usage_stats()
print(f"Storage used: {usage.storage_bytes_used:,} bytes")
print(f"Collections: {usage.collections_count}/{usage.collections_limit}")
print(f"Tier: {usage.tier}")
print(f"Replicating to: {', '.join(usage.active_regions)}")

Intelligent Global Routing

Your requests are automatically routed to the optimal region:

# No configuration needed - routing is automatic!
# Requests from the US  → the US region
# Requests from the EU  → the EU region
# Requests from Asia    → the Asia-Pacific region

🔧 Advanced Usage

Batch Operations

Efficient batch processing with automatic optimization:

# Bulk insert thousands of points (ids are unsigned integers or UUID strings)
large_batch = [
    {"id": i, "vector": [...], "payload": {...}}
    for i in range(10000)
]

# Automatically optimized batch size and routing
client.upsert("large_collection", large_batch)

Complex Filtering

Full compatibility with qdrant-client filters. Three clauses, and only three: must, must_not, should.

results = client.search(
    collection_name="products",
    query_vector=[...],
    query_filter={
        "must": [
            {"key": "category", "match": {"value": "electronics"}},
            {"key": "price", "range": {"gte": 100, "lte": 1000}}
        ],
        "must_not": [
            {"key": "status", "match": {"value": "discontinued"}}
        ],
    },
    limit=20
)

The same clauses are available as a typed object, if you prefer it to a dict:

from aetherfy_vectors.models import Filter

results = client.search(
    collection_name="products",
    query_vector=[...],
    query_filter=Filter(
        must=[{"key": "category", "match": {"value": "electronics"}}],
        must_not=[{"key": "status", "match": {"value": "discontinued"}}],
    ),
    limit=20,
)

A clause name outside those three raises ValidationError before the request leaves the process — including mustNot, which is the JavaScript SDK's spelling of must_not. Unknown clauses are never forwarded and never dropped: writing the wrong one is an error you see, not a filter that quietly matches everything. The same rule applies everywhere a filter is accepted — search, scroll, scroll_iter, count, delete.

Context Manager Support

with AetherfyVectorsClient(api_key="your_key", workspace="default") as client:
    results = client.search("collection", [0.1, 0.2, 0.3])
    # Automatic cleanup

🔁 Iterating Large Collections

For bulk reads, use scroll_iter() rather than scroll(limit=…). The iterator pages transparently and stays within the server's per-request caps (1000 points/call, 10 MB/response):

for point in client.scroll_iter("my_collection", batch_size=256):
    process(point)

# With a filter and selective payload/vector return
for point in client.scroll_iter(
    "my_collection",
    batch_size=512,
    scroll_filter={"must": [{"key": "status", "match": {"value": "active"}}]},
    with_payload=True,
    with_vectors=False,
):
    process(point)

batch_size is the page size for one HTTP round trip (max 1000 server-side). The iterator handles cursor management, page exhaustion, and pagination errors — no offset bookkeeping in user code.

✏️ Editing Payload on Existing Points

Three operations on the payload of points that already exist — no need to re-upsert vectors:

# MERGE: add or update keys, leave others alone
client.set_payload(
    "my_collection",
    {"reviewed": True, "reviewer": "alice"},
    [point_id],
)

# OVERWRITE: replace the entire payload object
client.overwrite_payload(
    "my_collection",
    {"category": "X"},   # all other keys are dropped
    [point_id],
)

# DELETE: remove specific keys, leave others alone
client.delete_payload(
    "my_collection",
    ["draft_field", "stale_score"],
    [point_id],
)

Each call accepts up to 512 points in one round trip; for larger mutations, batch on the caller side. The semantics map exactly to qdrant's set_payload / overwrite_payload / delete_payload so existing patterns transfer.

🧠 Memory SDK — Iter, Bulk-load, set_metadata

The Memory layer (aetherfy_memory) layers Namespace and Thread abstractions on top of AetherfyVectorsClient. Three additions worth knowing once you go past add() / search():

Iterating a namespace or a thread

from aetherfy_memory import MemoryClient
memory = MemoryClient(api_key="afy_live_…", workspace="my-bot")

ns = memory.namespace("customer-42")
for point in ns.iter(batch_size=256):
    process(point)

# Threads have iter_history() — yields messages in ts order across the
# whole conversation. Distinct from history(limit=N), which caps at 5000
# in memory for the most-recent slice.
thread = memory.thread("conv-99")
for msg in thread.iter_history(order="asc"):
    print(msg.role, msg.content)

Use history(limit=N) for "show me the last N messages" (bounded, fast). Use iter_history() for "walk every message in this thread" (paged, memory-bounded by the iterator).

Bulk-loading memories

add_many() and append_many() batch into a single client.upsert so N items become 1 round trip. IDs are returned in input order; missing IDs are auto-generated as canonical UUIDs (the same format iter() and retrieve() yield back, so equality comparisons just work).

items = [
    {"text": "first",  "vector": embed("first"),  "metadata": {"src": "a"}},
    {"text": "second", "vector": embed("second"), "metadata": {"src": "b"}},
]
ids = ns.add_many(items)            # single round trip; preserves input order

# Threads use append_many — role/content/ts payloads, ts auto-set per
# message when omitted (each message gets its own ts, not one shared).
msgs = [
    {"role": "user",      "content": "hi",    "vector": embed("hi")},
    {"role": "assistant", "content": "hello", "vector": embed("hello")},
]
ids = thread.append_many(msgs)

Threads have no add_many() — a Thread is not a Namespace subclass (they share a scope base but declare their own write API), so there is no add_many to call. add_many writes text/metadata payloads, which don't fit a thread's role/content/ts schema. Use append_many() on threads.

set_metadata — atomic replace, explicit-compose merge

set_metadata() replaces the entire metadata sub-key. set_metadata({"tag": "x"}) nukes every other key. Use merge_metadata() if you want additive updates that preserve existing keys. Reserved fields (text for Namespace; role/content/ts for Thread) are untouched either way.

ns.set_metadata(point_id, {"reviewed": True, "score": 0.92})

To merge into existing metadata via the explicit-compose pattern (race visible at the call site, no atomicity guarantee):

current = ns.retrieve([point_id])[0]["payload"].get("metadata", {})
current.update({"reviewed": True})
ns.set_metadata(point_id, current)

If two callers run this concurrently, one update wins and the other sees its read be stale — by design, you see that race in your own code rather than have the SDK hide it.

merge_metadata — atomic per-point partial merge

merge_metadata({"tag": "x"}) adds/updates the listed keys and leaves every other key untouched. Concurrent patches to different keys all land atomically; concurrent writes to the same key resolve via last-writer-wins per the storage operation order. Raises PointNotFoundError if the point doesn't exist. Reserved keys (text on Namespace; role, content, ts on Thread) cannot appear in the partial — raises a local ValueError before the request is sent.

ns.merge_metadata(point_id, {"reviewed": True})
ns.merge_metadata(point_id, {"score": 0.92})
# final metadata: original keys + reviewed + score

delete_metadata_keys — atomic key removal

delete_metadata_keys(point_id, ["tag", "score"]) removes the listed keys from metadata; keys not in the list are left untouched. Raises PointNotFoundError if the point doesn't exist. Reserved keys cannot appear in the keys list (same set as merge_metadata).

ns.delete_metadata_keys(point_id, ["draft", "stale_score"])

📐 Limits

Two axes constrain a single call: per-request size (PRS) and requests per minute (RPM). Both axes return a 4xx with a structured error.code when they fire — no surprise 5xx, no silent truncation.

Class Endpoint Cap
READS scroll · search · retrieve ≤ 1000 points/call · ≤ 10 MB/response
WRITES upsert ≤ 10 K vectors/call · streaming
payload edits · batch delete ≤ 512 points/call

Upserts stream — there is no body-size cap on the public upsert URL. The 10 K vectors/call is a defensive request-level ceiling, not a body limit; one call can upload millions of bytes via byte-target chunking on the receiving end. For bulk reads, use scroll_iter() — it pages transparently and stays within both quotas.

requests_per_minute is a sliding-window minutely cap derived from your subscription tier. When it fires, the SDK raises RateLimitExceededError with a structured retry_after (seconds); PRS violations raise ValidationError (400) or surface as 413 RESPONSE_TOO_LARGE for oversized response bodies.

🤝 Multi-Agent Workspaces

Every collection lives in exactly one workspace, and every account starts with one named default. A workspace decides where a collection is reachable from: agents in the same workspace see each other's collections, and an agent reaches another workspace only if the account owner granted it access.

Collection names are unique per workspace: invoices in customer-a and invoices in customer-b are two collections.

Choosing the workspace

from aetherfy_vectors import AetherfyVectorsClient

client = AetherfyVectorsClient(
    api_key="afy_live_your_api_key_here",
    workspace="invoice-pipeline",  # All operations are scoped to this workspace
)

workspace defaults to "auto": inside an agent deployed on Aetherfy, the agent's own workspace, by id, from AETHERFY_WORKSPACE_ID (the control plane injects it on every agent). A workspace rename never breaks an agent, because it is addressed by id.

import os

# Inside a deployed agent: its own workspace, nothing to configure.
client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

There is no "no workspace". workspace=None, an empty name, and "auto" outside an agent (where AETHERFY_WORKSPACE_ID is not set) raise ValueError at construction, saying to pass a workspace name or run inside an agent. client.workspace is the name (None when built from the id) and client.workspace_id the id (None when given a name).

"auto" is only the agent's home workspace. Code that works across several workspaces (one per customer, say) passes each workspace's name explicitly, one client per workspace.

How scoping works

You always use the collection's own name; the client sends it on the workspace's routes:

# Create a collection in the client's workspace
from aetherfy_vectors.models import VectorConfig, DistanceMetric

client.create_collection("documents", VectorConfig(size=768, distance=DistanceMetric.COSINE))

# Search — no need to know the full scoped name
results = client.search("documents", query_vector=embedding, limit=10)

# List — only returns collections in your workspace
collections = client.get_collections()
# → [Collection(name='documents', ...)]  (short names, not scoped names)

Multi-agent example

# Agent A: extractor
extractor = AetherfyVectorsClient(api_key=api_key, workspace="invoice-pipeline")
extractor.create_collection("raw-invoices", VectorConfig(size=768, distance=DistanceMetric.COSINE))
extractor.upsert("raw-invoices", extracted_points)

# Agent B: classifier — same workspace, sees Agent A's collection
classifier = AetherfyVectorsClient(api_key=api_key, workspace="invoice-pipeline")
results = classifier.search("raw-invoices", query_vector=embedding, limit=20)

Workspace errors

from aetherfy_vectors import WorkspaceNotFoundError

try:
    client.get_collections()
except WorkspaceNotFoundError as e:
    # No such workspace in this account. A typo is never read as "empty".
    print(f"Unknown workspace: {e.workspace or e.workspace_id}")

collection_exists() returns False only when the workspace exists and the collection is not in it; an unknown workspace raises.

When a request is retried

One rule, the same in the Python and JS SDKs (aetherfy_vectors/retry_policy.py). Up to 3 retries, waiting about 1 s, 2 s, then 4 s (each jittered to 50-100%), within a call's timeout.

Failure Retried for
Refused before anything ran: 503 WORKSPACE_LOOKUP_UNAVAILABLE, COLLECTION_LOOKUP_UNAVAILABLE, PLACEMENT_UNAVAILABLE; a connection never made (DNS, refused, connect timeout); 429 with a Retry-After every operation
Ambiguous, the request may have run: a read timeout, a reset after sending, any other 5xx only operations whose repeat gives the same result (below)
Any other 4xx never
Operations Class
get_collections, get_collection, collection_exists, get_schema, analyze_schema, search, retrieve, scroll, count, get_usage_stats reads: repeatable
upsert, set_payload, overwrite_payload, delete_payload, create_field_index, set_schema writes keyed by id: repeatable
create_collection, delete_collection, delete (points), delete_schema, delete_field_index once: a repeat after a hidden success reports a false error, so an ambiguous failure is raised after one attempt

set_schema is repeatable because it is sent without If-Match: a repeat writes the same schema again. A conditional write would get a false 412 on a repeat after a hidden success, so it would be classed once.

When the retries run out, the last error is raised (ServiceUnavailableError, RequestTimeoutError or NetworkError).

The client already retries under this rule. To retry more around your own operation, say whether repeating it gives the same result: REPEATABLE (a read, a write keyed by id) or ONCE (a create, a delete). The same failures are retried as above; a 4xx never is.

from aetherfy_vectors import retry_with_backoff, REPEATABLE

results = retry_with_backoff(
    lambda: client.search("collection", vector),
    REPEATABLE,
    max_retries=3,
    base_delay=1.0,
)

Renaming a collection, and moving one to another workspace, are done from the dashboard or the CLI (afy collections update), not from the SDK. Code still using the old name gets CollectionNotFoundError.

Tip: Create workspaces in the dashboard or with the CLI (afy workspaces create invoice-pipeline). Agents deployed to a workspace receive its id as AETHERFY_WORKSPACE_ID.

🤖 Running as an Agent

Code deployed to Aetherfy as an agent gets a set of helpers in the same distribution, under aetherfy_agent. Nothing to add to your requirements: the standard runtime image preinstalls aetherfy-vectors, and a version you pin yourself wins over it. A custom container installs it like any other package.

Each of these is a thin wrapper over a platform contract the docs already publish; the helper exists so the contract stops being copied into every task.

from aetherfy_agent import payload, machine, fan_out, spawn

# This run's input. A scheduled fire sends none, so {} is the normal case.
data = payload()

# The machine this run is on: whole numbers, not the strings the env carries.
shape = machine()

# An in-machine pool. Results come back in INPUT order and no failure is
# swallowed: the lowest-indexed exception is re-raised once every worker has
# finished. Width defaults to vcpus x 8 for threads, the default pool.
answers = fan_out(summarise, data.get("items", []))

# CPU-bound work wants processes. The default width follows the pool:
# threads get vcpus x 8 (they mostly wait), processes get vcpus.
digests = fan_out(hash_one, data.get("files", []), kind="processes")

# Run a different task agent, on its own machine, with its own lifecycle.
run = spawn("nightly-rollup", {"date": "2026-09-07"})
print(run.spawn_id, run.region, run.status)

fan_out prints one line to the run's logs before it starts, so how wide a run went is visible after the fact:

aetherfy: fanning out 32 wide on 4 vCPU / 8192 MB (120 tasks)

Spawning has three outcomes worth telling apart. The payload cap and the concurrent-run cap get their own types; everything else carries the platform's stable error code, which is the thing to branch on:

from aetherfy_agent import spawn
from aetherfy_agent.exceptions import (
    PayloadTooLarge,
    SpawnError,
    TooManyRunsInFlight,
)

try:
    spawn("nightly-rollup", {"date": "2026-09-07"})
except PayloadTooLarge as exc:
    # The payload is for parameters and references, not data. Write the data to
    # a collection and pass its id.
    print(exc.payload_bytes, "exceeds", exc.max_bytes)
except TooManyRunsInFlight as exc:
    # The one refusal here worth retrying: runs are finishing all the time.
    # The cap is the ACCOUNT's and is set by your plan; `limit` names which
    # plan limit was hit, and `max_in_flight_runs` is None on an uncapped plan.
    print("waiting on", exc.in_flight_count, "of", exc.max_in_flight_runs)
except SpawnError as exc:
    print(exc.error_code)

Returning a result, and reading one back

Output on Aetherfy mirrors input. A task writes its answer with write_result; whoever started the run reads it back off the run itself, so a parent hears from a child in another region with no side channel between them.

from aetherfy_agent import write_result

# In the child task. The mirror of payload(): a file on the machine, nothing
# over the network. Returning nothing is the normal case, so most tasks never
# call this at all.
write_result({"rows": 128, "date": "2026-09-08"})
from aetherfy_agent import spawn, wait, result

# In the parent. wait() holds one request open instead of polling; a run that
# has not finished in time comes back exactly as it stands, which is not an
# error — read `state` and call again.
run = spawn("nightly-rollup", {"date": "2026-09-08"})
finished = wait(run.spawn_id, timeout_seconds=45)

if finished.state == "completed":
    print(finished.result)

# result() is the same read without the waiting.
now = result(run.spawn_id)
print(now.state, now.has_result, now.result_error)

The result shares the payload's inline cap — one number bounding both directions — and it is for answers and references, not data. Anything larger belongs in a collection, with its id in the result:

from aetherfy_agent import write_result
from aetherfy_agent.exceptions import ResultTooLarge

try:
    write_result(everything)
except ResultTooLarge as exc:
    print(exc.result_bytes, "exceeds", exc.max_bytes)
    write_result({"collection": "nightly-rollup", "rows": len(everything)})

Reading a run has the same shape as spawning it: the two refusals worth telling apart get their own types, and everything else carries the platform's stable error code.

from aetherfy_agent import result
from aetherfy_agent.exceptions import RunAccessDenied, RunNotFound, RunReadError

try:
    result(run_id)
except RunNotFound:
    print("no run has that id")
except RunAccessDenied:
    print("that run belongs to another account")
except RunReadError as exc:
    print(exc.error_code)

With the agent's own AETHERFY_API_KEY, a read the control plane does not allow that key (which ones it allows) is refused with RunReadError and error_code AUTH_AGENT_KEY_OUT_OF_SCOPE.

Full contract, including the environment variables behind every call: docs.aetherfy.com/agents/task-contract.

🧩 Payload Schemas

Collections can carry an optional payload schema that the SDK validates against before upsert — catching malformed payloads client-side without a round trip. Schemas are cached and automatically revalidated when they change server-side (via ETag).

from aetherfy_vectors import Schema, FieldDefinition

schema = Schema(
    fields={
        "title":    FieldDefinition(type="string",  required=True),
        "price":    FieldDefinition(type="float",   required=True),
        "tags":     FieldDefinition(type="array",   required=False, element_type="string"),
        "in_stock": FieldDefinition(type="boolean", required=False),
    },
    description="Product catalog payloads",
)

# enforcement: "off" (no validation), "warn" (log warnings), "strict" (raise on violation)
etag = client.set_schema("products", schema, enforcement="strict")

# Inspect or remove the schema
current = client.get_schema("products")   # returns None if no schema is defined
client.delete_schema("products")

Infer a schema from existing data

analysis = client.analyze_schema("products", sample_size=1000)
print(analysis.suggested_schema)   # a Schema you can set_schema() directly

Schema violations raise SchemaValidationError (with a list of per-field errors). If the server reports a stale schema (412 Precondition Failed), the SDK auto-refreshes the cache and retries.

📊 Performance Comparison

Feature Local Qdrant Aetherfy Vectors
Global Latency Single region, wherever you host it Served from the nearest region
Caching None Built in
DevOps Required High Zero
Auto-Failover Manual Automatic
Global Replication Manual Automatic
Analytics Limited Built-in

🛠️ Environment Setup

API Key Configuration

Set your API key using environment variables (recommended):

# Either of these is read automatically
export AETHERFY_API_KEY="afy_live_your_api_key_here"
export AETHERFY_VECTORS_API_KEY="afy_live_your_api_key_here"

Or pass it directly:

client = AetherfyVectorsClient(api_key="afy_live_your_api_key_here", workspace="default")

Relevant environment variables:

Variable Purpose
AETHERFY_API_KEY Primary API key
AETHERFY_VECTORS_API_KEY Alternative API key (useful when the same process talks to multiple Aetherfy services)
AETHERFY_WORKSPACE_ID The agent's workspace id, read by workspace="auto" (the default). Set automatically on deployed agents
AETHERFY_VECTORS_URL Pin the client to a specific endpoint URL. Set automatically by the control-plane on deployed agents. Wins over api_region= if both are set.
AETHERFY_VECTORS_API_REGION Equivalent to api_region= constructor arg. Local-dev / debugging only.

Local development across regions

Production agents have AETHERFY_VECTORS_URL injected by the control-plane — that's the URL they reach the regional backend through, and it takes precedence over api_region=. For local development (no env var injected), you can pin a client to a specific regional endpoint:

client = AetherfyVectorsClient(
    api_key="afy_test_...",
    workspace="default",
    api_region="eu-central-1",  # 'us-east-1' | 'eu-central-1' | 'ap-southeast-1'
)

The first call resolves api_region against GET /api/v1/regions on the default global endpoint and caches the result on the client instance. If both AETHERFY_VECTORS_URL and api_region= are set, the env var wins and a warning is logged — that's the production-agent protection rule.

api_region selects which regional endpoint to connect to — it is a transport/routing override, not where a collection's data lives. To control collection placement, pass regions= to create_collection (see below).

Python Version Support

  • Minimum: Python 3.9
  • Recommended: Python 3.10+
  • Tested: Python 3.9, 3.10, 3.11, 3.12

📚 Complete API Reference

Collection Management

# Create collection — returns the created Collection (with its resolved
# `regions` echoed back by the server). Pass `regions=[...]` to pin the
# collection to a subset of your scope; omit it to default to your full
# scope. (Distinct from the constructor's `api_region`, which only picks
# the endpoint to connect to.)
collection = client.create_collection(
    collection_name, vectors_config, distance=None, description=None, regions=None
)

# List collections
collections = client.get_collections()

# Get collection info
info = client.get_collection(name)

# Check existence
exists = client.collection_exists(name)

# Delete collection
client.delete_collection(name)

Payload Indexes

A payload filter on an UNINDEXED key is scanned, not looked up. Index any key you filter on for every read — a tenant id, a status, a timestamp you range over.

# field_schema: "keyword" | "integer" | "float" | "bool" | "geo" |
#               "datetime" | "uuid" | "text", or a parameterised object.
# Returns once the index is built, so a filter or an order_by scroll on the
# key can follow straight away. A large collection can take longer than the
# server's 25 s wait; the call then waits on, up to timeout= (default 600 s).
# Past it, RequestTimeoutError: the build carries on, and calling again waits.
client.create_field_index(collection_name, "tenant_id", "keyword")
client.create_field_index(collection_name, "ts", "integer", timeout=120)

# True when the collection exists, even if the field was never indexed.
# False only when the collection does not exist.
client.delete_field_index(collection_name, "tenant_id")

Point Operations

# Insert/update points
client.upsert(collection_name, points)

# Retrieve points
points = client.retrieve(collection_name, ids, with_payload=True, with_vectors=False)

# Delete points
client.delete(collection_name, point_ids_or_filter)

# Count points — count_filter takes a Filter or a plain dict, same as
# search / scroll / delete
count = client.count(collection_name, count_filter=None, exact=True)

Search Operations

# Vector search
results = client.search(
    collection_name,
    query_vector,
    limit=10,
    offset=0,
    query_filter=None,
    with_payload=True,
    with_vectors=False,
    score_threshold=None,
    search_params=None      # engine params, sent verbatim as the body's `params`
)

# Trade latency for recall: a larger hnsw_ef makes the graph walk visit more
# candidates. Omit search_params to keep the tuned server default (hnsw_ef=100).
results = client.search(collection_name, query_vector, search_params={"hnsw_ef": 256})

search_params is passed through untranslated — the API and Qdrant own the schema, so the SDK validates nothing and needs no release to track new params. Note that the server cache key is derived from the request body, so the same query at a different hnsw_ef is a separate cache entry (never a wrong hit).

Schema Management (Aetherfy-specific)

# Set, fetch, and remove a payload schema.
# enforcement defaults to "off" — pass "warn" or "strict" to have the
# server act on violations.
etag = client.set_schema(collection_name, schema, enforcement="off", description=None)
schema = client.get_schema(collection_name)           # None if not set
client.delete_schema(collection_name)

# Infer a schema from existing data
analysis = client.analyze_schema(collection_name, sample_size=1000)  # 100–10000

# Cache control (rarely needed)
client.refresh_schema(collection_name)
client.clear_schema_cache(collection_name=None)       # None clears all

Usage statistics (Aetherfy-specific)

usage = client.get_usage_stats()

usage.storage_bytes_used     # int  — bytes stored across every collection
usage.storage_limit_bytes    # int | None — None on an unlimited tier
usage.collections_count      # int  — active collections
usage.collections_limit      # int | None — None on an unlimited tier
usage.tier                   # str  — the plan's tier name
usage.active_regions         # list[str] — union of your collections' regions
usage.usage_percentage       # int  — storage %, 0 when there is no limit

🚨 Error Handling

The SDK provides detailed error handling compatible with qdrant-client:

from aetherfy_vectors.exceptions import (
    AetherfyVectorsException,   # base class for all SDK errors
    AuthenticationError,
    CollectionNotFoundError,
    PointNotFoundError,
    RateLimitExceededError,
    ServiceUnavailableError,
    ValidationError,
    RequestTimeoutError,
    NetworkError,
    SchemaValidationError,
    SchemaNotFoundError,
    CollectionInUseError,
    QuotaExceededError,
    WorkspaceNotFoundError,
    NotFoundError,              # parent of every not-found error above
)

try:
    results = client.search("nonexistent", [0.1, 0.2, 0.3])
except CollectionNotFoundError as e:
    print(f"Collection not found: {e}")
except WorkspaceNotFoundError as e:
    print(f"Workspace not found: {e.workspace or e.workspace_id}")
except AuthenticationError as e:
    print(f"Invalid API key: {e}")
except RateLimitExceededError as e:
    print(f"Rate limit exceeded. Retry after: {e.retry_after}s")
except SchemaValidationError as e:
    for violation in e.errors:
        print(violation)
except QuotaExceededError as e:
    print(f"Quota '{e.quota_type}' exceeded: {e.current}/{e.limit}")

🔧 Configuration Options

client = AetherfyVectorsClient(
    api_key="your_api_key",                   # Required (or set AETHERFY_API_KEY)
    endpoint="https://vectors.aetherfy.com",  # Optional: Custom endpoint
    timeout=30.0,                             # Optional: Request timeout (seconds)
    workspace="default",                      # A workspace name, or "auto" (the default):
                                              #   inside an agent, AETHERFY_WORKSPACE_ID
)

📈 Monitoring & Observability

Built-in Dashboard Data

# Quota consumption, for a plan-usage dashboard
usage = client.get_usage_stats()

dashboard_data = {
    "storage_bytes": usage.storage_bytes_used,
    "storage_limit_bytes": usage.storage_limit_bytes,   # None = unlimited
    "storage_percent": usage.usage_percentage,
    "collections": usage.collections_count,
    "collections_limit": usage.collections_limit,       # None = unlimited
    "tier": usage.tier,
    "regions": usage.active_regions,
}

The fields are the endpoint's own, verbatim — no derived percentages beyond the one it serves, and no unit conversion. Anything else you want (a collections percentage, megabytes) is a calculation at your call site, where the "unlimited" sentinels are yours to handle.

Aetherfy does not expose latency or cache-hit telemetry through this SDK. Measure request latency at your own call site — it is the only number that reflects what your application actually experienced.

Health Checks

def health_check():
    try:
        collections = client.get_collections()
        return {"status": "healthy", "collections": len(collections)}
    except Exception as e:
        return {"status": "unhealthy", "error": str(e)}

🔗 Migration Guide

Step-by-Step Migration

  1. Install aetherfy-vectors:

    pip install aetherfy-vectors
    
  2. Get your API key from the Aetherfy dashboard

  3. Update imports:

    # from qdrant_client import QdrantClient
    from aetherfy_vectors import AetherfyVectorsClient
    
  4. Update initialization:

    # client = QdrantClient(host="localhost", port=6333)
    client = AetherfyVectorsClient(api_key="your_api_key", workspace="default")
    
  5. Run your tests. Calls to the methods in the table below, with qdrant-client arguments passed by keyword, need no change. A refused argument or a query_points call fails at once, naming what to change.

  6. Optional: Add analytics calls for insights

  7. Deploy and enjoy global performance! 🚀

Migration Compatibility

These qdrant-client methods exist here under the same name, taking the same keyword arguments: create_collection, delete_collection, get_collections, get_collection, collection_exists, upsert, delete, retrieve, search, scroll, count, set_payload, overwrite_payload, delete_payload, close.

The contract is qdrant-client 1.15.1's signatures, the last release that still has search. A keyword argument that qdrant-client doesn't have raises TypeError, the same as any Python function would: a misspelt or outdated name fails straight away instead of being dropped without a word. The qdrant-client arguments these methods don't name themselves are handled one by one:

Method qdrant-client argument Here Why
upsert, delete, set_payload, overwrite_payload, delete_payload wait accepted, no effect Every point write is committed before the call returns, so wait=True already holds and wait=False gets a stronger guarantee
upsert, delete, set_payload, overwrite_payload, delete_payload ordering refused unless 'weak' Writes use Qdrant's default weak ordering; medium and strong are not provided
upsert, delete, set_payload, overwrite_payload, delete_payload, retrieve, scroll, search, count shard_key_selector refused unless None Custom shard keys are not supported; placement is per collection (regions=)
retrieve, scroll, search consistency refused unless None A read is answered by the region you are connected to; multi-replica read consistency is not provided
search append_payload refused unless True Deprecated by qdrant-client itself; use with_payload=False
create_collection sparse_vectors_config refused unless None Only dense vectors are supported
create_collection shard_number, sharding_method refused unless None Sharding is managed by the service
create_collection replication_factor, write_consistency_factor refused unless None Replication across regions is managed by the service (regions=)
create_collection on_disk_payload, hnsw_config, optimizers_config, wal_config, quantization_config, strict_mode_config refused unless None Storage and index settings are fixed by the service and would not be applied
create_collection init_from refused unless None Creating a collection from another one is not supported
close grpc_grace accepted, no effect There is no gRPC channel to wait on
create_collection, delete_collection, retrieve, scroll, search, count timeout honoured A deadline for the whole call, retries and backoff included, as in qdrant-client. The constructor's timeout is different: it bounds each attempt, and writes are retried up to 3 times
scroll order_by honoured A payload key or {"key": ..., "direction": ...}, sent as-is

A refused argument raises TypeError naming it and the reason, so a migration finds out on its first run instead of in production.

The constructor is the one call the migration replaces outright, so it accepts none of QdrantClient's arguments. It takes only what it documents (api_key, endpoint, api_region, timeout, workspace), and anything else raises TypeError. Code written before 1.2.0 that passes region= now fails at construction. Rename it to api_region=: until 1.2.0 the old name was accepted and ignored, which quietly routed the client to the default endpoint.

Positional arguments are accepted only as far as a method's parameter order matches qdrant-client's. Past that point parameters are keyword-only, so a positional call that would have bound the wrong parameter raises TypeError. For example, search's third positional is qdrant-client's query_filter but was this SDK's limit.

🤝 Contributing

We welcome contributions! Open an issue or a pull request on GitHub.

Development Setup

# Clone the repository
git clone https://github.com/l-td/aetherfy-vectors-python-sdk.git
cd aetherfy-vectors-python-sdk

# Install development dependencies
pip install -r requirements-dev.txt

# Run tests
pytest

# Format code
black aetherfy_vectors/

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🆘 Support

🌟 Why Choose Aetherfy Vectors?

For Solo Developers

  • Zero setup time - start building immediately
  • Predictable pricing - no surprise infrastructure costs
  • Global reach - your users get fast responses worldwide

For Teams

  • No DevOps overhead - focus on your product, not infrastructure
  • Built-in monitoring - comprehensive analytics out of the box
  • Reliable performance - automatic failover

For Enterprises

  • Global scalability - handles millions of vectors effortlessly
  • Security first - enterprise-grade authentication and audit logs
  • Cost effective - pay only for what you use, no idle infrastructure

Ready to experience global vector search? Get your API key and migrate in minutes! 🚀

Metadata

Release files for aetherfy-vectors 1.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aetherfy-vectors 1.2.1
File Size Uploaded Provenance
aetherfy_vectors-1.2.1.tar.gz 133.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for aetherfy-vectors 1.2.1
File Interpreter ABI Platform
aetherfy_vectors-1.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 247.2 kB

Release files / aetherfy_vectors-1.2.1.tar.gz

Download URL aetherfy_vectors-1.2.1.tar.gz
Size 133.7 kB
Tags Source
SHA-256 checksum
How to use checksums
6e1b4cb1200f893f1339c6289e849901b60de3b7026845a5a8e91653043cddb6
BLAKE2b-256 checksum
How to use checksums
272d3af5a9e05adb2bf0d97bb25b703352eb3ad4838a8f869306c52a3febd8ed
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.22

Release files / aetherfy_vectors-1.2.1-py3-none-any.whl

Download URL aetherfy_vectors-1.2.1-py3-none-any.whl
Size 113.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c96e4005b116c526b3dc0031c7afc645cf774a5895429ac9690649e1d54545c5
BLAKE2b-256 checksum
How to use checksums
55c9483bdca8e9463076a97e27c6a121ad1678554bd63a5b51a3456b14936601
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.22

Security for aetherfy-vectors 1.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page