---
slug: vectors/sdk
title: SDK reference
kind: reference
surface: vectors
summary: Reference for the Aetherfy vector SDKs — the aetherfy-vectors Python and JavaScript packages, their client constructors, complete method surface, model shapes, point ID rules, and the per-language divergences that change results.
sources:
  - aetherfy-vectors-python-sdk:setup.py
  - aetherfy-vectors-python-sdk:aetherfy_vectors/__init__.py
  - aetherfy-vectors-python-sdk:aetherfy_vectors/client.py
  - aetherfy-vectors-python-sdk:aetherfy_vectors/models.py
  - aetherfy-vectors-python-sdk:aetherfy_vectors/auth.py
  - aetherfy-vectors-js-sdk:package.json
  - aetherfy-vectors-js-sdk:src/index.ts
  - aetherfy-vectors-js-sdk:src/client.ts
  - aetherfy-vectors-js-sdk:src/models.ts
  - aetherfy-vectors-js-sdk:src/auth.ts
---

# Aetherfy vector SDK reference

## Installing the Aetherfy vector SDKs

Both Aetherfy client libraries are distributed as `aetherfy-vectors` — the same
name in PyPI and npm. The npm package is **unscoped**: it is `aetherfy-vectors`,
not `@aetherfy/vectors`.

| | Python | JavaScript / TypeScript |
|---|---|---|
| Distribution name | `aetherfy-vectors` | `aetherfy-vectors` |
| Install | `pip install aetherfy-vectors` | `npm install aetherfy-vectors` |
| Version | 1.0.0 | 1.0.0 |
| Minimum runtime | Python >= 3.9 | Node >= 20 |
| Packaging metadata | `setup.py` (there is no `pyproject.toml`) | `package.json` |

The Aetherfy Python distribution ships **two import packages** from the one
install: `aetherfy_vectors` (the vector client) and `aetherfy_memory` (the memory
layer).

```bash
pip install aetherfy-vectors
```

```python
import aetherfy_vectors
import aetherfy_memory
```

The Aetherfy JavaScript package declares an `exports` map with only the `"."`
entry. There are no subpath entry points, so `aetherfy-vectors/memory` does
**not** resolve. Import everything — including the memory-layer classes — from
the package root.

```bash
npm install aetherfy-vectors
```

```typescript
// Correct: everything comes from the package root.
import { AetherfyVectorsClient, MemoryClient } from 'aetherfy-vectors';

console.log(typeof AetherfyVectorsClient, typeof MemoryClient);
```

## Importing from the Aetherfy SDKs

The Aetherfy Python root package re-exports the client, the exception classes,
and six models — but **not** every type you will need. This split is exact:

| Exported from the `aetherfy_vectors` root | Only in `aetherfy_vectors.models` |
|---|---|
| `Point`, `SearchResult`, `Collection`, `Schema`, `FieldDefinition`, `AnalysisResult` | `VectorConfig`, `DistanceMetric`, `Filter` |

Those six root-exported models also resolve from `aetherfy_vectors.models`, so
both `from aetherfy_vectors import Point` and
`from aetherfy_vectors.models import Point` work — pick one spelling and stay
with it. `VectorConfig`,
`DistanceMetric`, and `Filter` are absent from the root `__all__` and have only
the `.models` path.

```python
# Root package: client, models, exceptions.
from aetherfy_vectors import (
    AetherfyVectorsClient,
    Point,
    SearchResult,
    Collection,
    AetherfyVectorsException,
    AuthenticationError,
    RateLimitExceededError,
    ServiceUnavailableError,
    SchemaValidationError,
    SchemaNotFoundError,
    PartialUpsertError,
    CollectionInUseError,
    CollectionInOtherRegionError,
    QuotaExceededError,
)

# NOT re-exported at the root — import these from aetherfy_vectors.models.
from aetherfy_vectors.models import VectorConfig, DistanceMetric, Filter

print(VectorConfig(size=4, distance=DistanceMetric.COSINE))
```

The Aetherfy JavaScript root export includes the client (also the default
export), `createClient()`, the `DistanceMetric` enum, every exception class, the
memory-layer classes, `DEFAULT_VECTOR_SIZE`, and the public types. Two type names
are **not** root-exported: `ScrollOptions` and `ScrollResult`. Do not put them in
an import statement.

```typescript
import AetherfyVectorsClient, {
  createClient,
  DistanceMetric,
  DEFAULT_VECTOR_SIZE,
  MemoryClient,
  Namespace,
  Thread,
} from 'aetherfy-vectors';
import type {
  ClientConfig,
  Collection,
  CountOptions,
  FieldDefinition,
  Filter,
  Point,
  RetrieveOptions,
  Schema,
  ScrollIterOptions,
  ScrollPoint,
  SearchOptions,
  SearchResult,
  VectorConfig,
} from 'aetherfy-vectors';

const config: ClientConfig = { apiKey: process.env.AETHERFY_API_KEY };
const client = new AetherfyVectorsClient(config);
console.log(DEFAULT_VECTOR_SIZE, DistanceMetric.COSINE, typeof createClient);
console.log(typeof MemoryClient, typeof Namespace, typeof Thread);
client.dispose();
```

## Constructing an Aetherfy client

The Aetherfy Python client is synchronous; the Aetherfy JavaScript client is
asynchronous. Both default to the endpoint `https://vectors.aetherfy.com`.

**Python** — `AetherfyVectorsClient(api_key=None, endpoint=None, api_region=None, timeout=30.0, workspace=None, **kwargs)`

| Parameter | Default | Meaning |
|---|---|---|
| `api_key` | `None` | API key; falls back to the environment |
| `endpoint` | `None` | Base URL; defaults to `https://vectors.aetherfy.com` |
| `api_region` | `None` | Pin the client to one region |
| `timeout` | `30.0` | Request timeout in **seconds** |
| `workspace` | `None` | Scope all collection calls to a workspace |

**JavaScript** — `new AetherfyVectorsClient(config: ClientConfig = {})`

| `ClientConfig` field | Default | Meaning |
|---|---|---|
| `apiKey` | unset | API key; falls back to the environment |
| `endpoint` | unset | Base URL; defaults to `https://vectors.aetherfy.com` |
| `timeout` | `30000` | Request timeout in **milliseconds** |
| `workspace` | unset | Scope all collection calls to a workspace |
| `apiRegion` | unset | Pin the client to one region — see the region section below |

```python
import os

from aetherfy_vectors import AetherfyVectorsClient

client = AetherfyVectorsClient(
    api_key=os.environ["AETHERFY_API_KEY"],
    timeout=60.0,  # seconds
)
print(client.get_collections())
client.close()
```

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({
  apiKey: process.env.AETHERFY_API_KEY,
  timeout: 60000, // milliseconds
});

console.log(await client.getCollections());
client.dispose();
```

## Authenticating an Aetherfy client

When you do not pass a key, both Aetherfy SDKs resolve one from the environment
in this order:

| Order | Environment variable |
|---|---|
| 1 | `AETHERFY_API_KEY` |
| 2 | `AETHERFY_VECTORS_API_KEY` |

Keys are validated client-side against `^afy_(live|test)_[a-zA-Z0-9]{16,}$`, so a
truncated or mistyped key fails before any Aetherfy request is made. That regex
is a deliberately permissive client-side check, not the issued format: Aetherfy
generates `afy_live_` or `afy_test_` followed by exactly 32 hexadecimal
characters — see [API keys](/platform/api-keys) for the canonical format. Manage
keys at [https://app.aetherfy.com/dashboard/settings/api-keys](https://app.aetherfy.com/dashboard/settings/api-keys).

```python
import os

from aetherfy_vectors import AetherfyVectorsClient

# Explicit — equivalent to leaving api_key unset when AETHERFY_API_KEY is present.
explicit = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])
implicit = AetherfyVectorsClient()

print(explicit.collection_exists("articles"), implicit.collection_exists("articles"))
explicit.close()
implicit.close()
```

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

// Explicit — equivalent to `new AetherfyVectorsClient()` when AETHERFY_API_KEY is set.
const explicit = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });
const implicit = new AetherfyVectorsClient();

console.log(
  await explicit.collectionExists('articles'),
  await implicit.collectionExists('articles'),
);
explicit.dispose();
implicit.dispose();
```

A note on the Aetherfy JavaScript exception namespace: the package contains two
different classes named `AuthenticationError`. The one defined in `src/auth.ts` is
re-exported from the package root as **`AuthError`** so the two do not collide.
`AuthenticationError` at the root is the transport-level one.

## Pinning an Aetherfy client to a region

The Aetherfy SDKs accept three region identifiers:

| Region |
|---|
| `us-east-1` |
| `eu-central-1` |
| `ap-southeast-1` |

Multi-region replication is a property of the plan, not the client: Free and
Starter accounts are single-region, with the region fixed by the first resource
created, and replication across regions begins at the tier named **Performance**.
Pinning a client does not change what your plan replicates.

In Python, pass `api_region` to the constructor:

```python
import os

from aetherfy_vectors import AetherfyVectorsClient

client = AetherfyVectorsClient(
    api_key=os.environ["AETHERFY_API_KEY"],
    api_region="eu-central-1",
)
print(client.get_collections())
client.close()
```

In JavaScript, `apiRegion` requires region discovery, which is asynchronous.
Passing `apiRegion` to the `new` constructor (without an endpoint or environment
override) **throws**. Use the static `create()` factory instead:

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

// Correct: `create` performs region discovery before returning a client.
const client = await AetherfyVectorsClient.create({
  apiKey: process.env.AETHERFY_API_KEY,
  apiRegion: 'eu-central-1',
});

console.log(await client.getCollections());
client.dispose();
```

## Collection operations in the Aetherfy SDKs

| Operation | Python | JavaScript |
|---|---|---|
| Create | `create_collection(collection_name, vectors_config, distance=None, description=None, regions=None) -> Collection` | `createCollection(collectionName, vectorsConfig, description?, regions?) -> Promise<Collection>` |
| Delete | `delete_collection(collection_name) -> bool` | `deleteCollection(collectionName) -> Promise<boolean>` |
| List | `get_collections() -> List[Collection]` | `getCollections() -> Promise<Collection[]>` |
| Exists | `collection_exists(collection_name) -> bool` | `collectionExists(collectionName) -> Promise<boolean>` |
| Get one | `get_collection(collection_name) -> Collection` | `getCollection(collectionName) -> Promise<Collection>` |

The Python signature carries a separate `distance` parameter alongside
`vectors_config`; the JavaScript signature does not — the distance lives inside
the config object. Aetherfy fixes the rest of the collection configuration
server-side, so there is no place to pass optimizer or index settings in either
SDK.

```python
import os

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

client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

collection = client.create_collection(
    collection_name="articles",
    vectors_config=VectorConfig(size=4, distance=DistanceMetric.COSINE),
    description="Article embeddings",
)
print(collection.name, collection.points_count, collection.regions)

print(client.collection_exists("articles"))
print([c.name for c in client.get_collections()])
print(client.delete_collection("articles"))
client.close()
```

```typescript
import { AetherfyVectorsClient, DistanceMetric } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });

const collection = await client.createCollection(
  'articles',
  { size: 4, distance: DistanceMetric.COSINE },
  'Article embeddings',
);
console.log(collection.name, collection.pointsCount, collection.regions);

console.log(await client.collectionExists('articles'));
console.log((await client.getCollections()).map((c) => c.name));
console.log(await client.deleteCollection('articles'));
client.dispose();
```

## Upserting points with the Aetherfy SDKs

`upsert` writes points, creating or replacing them by id. Aetherfy caps a single
upsert call at 10 000 points; see [limits](/vectors/limits).

| Operation | Python | JavaScript |
|---|---|---|
| Upsert | `upsert(collection_name, points) -> bool` | `upsert(collectionName, points) -> Promise<boolean>` |

Both Aetherfy SDKs accept two shapes for `points`. Python is typed
`Sequence[Union[Point, Dict[str, Any]]]`, so a `Point` dataclass and a plain
dict are equally valid; JavaScript is typed
`Point[] | Record<string, unknown>[]`. The Aetherfy quickstart writes plain
dicts and the Aetherfy examples write `Point(...)` — both are correct, and
there is no behavioural difference between them.

```python
import os

from aetherfy_vectors import AetherfyVectorsClient, Point

client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

ok = client.upsert(
    collection_name="articles",
    points=[
        Point(id=1, vector=[0.1, 0.2, 0.3, 0.4], payload={"lang": "en", "year": 2024}),
        Point(id=2, vector=[0.4, 0.3, 0.2, 0.1], payload={"lang": "fr", "year": 2025}),
    ],
)
print(ok)
client.close()
```

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });

const ok = await client.upsert('articles', [
  { id: 1, vector: [0.1, 0.2, 0.3, 0.4], payload: { lang: 'en', year: 2024 } },
  { id: 2, vector: [0.4, 0.3, 0.2, 0.1], payload: { lang: 'fr', year: 2025 } },
]);
console.log(ok);
client.dispose();
```

If some points are written and some are not, the Aetherfy SDKs raise
`PartialUpsertError`, which carries `.saved`, `.total`, and `.failed`.

## Searching with the Aetherfy SDKs

Python takes the search options as keyword arguments; JavaScript takes a
`SearchOptions` object.

**Python** — `search(collection_name, query_vector, limit=10, offset=0, query_filter=None, with_payload=True, with_vectors=False, score_threshold=None, search_params=None) -> List[SearchResult]`

**JavaScript** — `search(collectionName, queryVector, options: SearchOptions = {}) -> Promise<SearchResult[]>`

| Python keyword | JavaScript `SearchOptions` field | Default |
|---|---|---|
| `limit` | `limit` | `10` |
| `offset` | `offset` | `0` |
| `query_filter` | `queryFilter` | none |
| `with_payload` | `withPayload` | `True` / `true` |
| `with_vectors` | `withVectors` | `False` / `false` |
| `score_threshold` | `scoreThreshold` | none |
| `search_params` | `searchParams` | none |

```python
import os

from aetherfy_vectors import AetherfyVectorsClient

client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

results = client.search(
    collection_name="articles",
    query_vector=[0.1, 0.2, 0.3, 0.4],
    limit=5,
    query_filter={"must": [{"key": "lang", "match": {"value": "en"}}]},
    with_payload=True,
    with_vectors=False,
    score_threshold=0.5,
)
for hit in results:
    print(hit.id, hit.score, hit.payload)
client.close()
```

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });

const results = await client.search('articles', [0.1, 0.2, 0.3, 0.4], {
  limit: 5,
  queryFilter: { must: [{ key: 'lang', match: { value: 'en' } }] },
  withPayload: true,
  withVectors: false,
  scoreThreshold: 0.5,
});
for (const hit of results) {
  console.log(hit.id, hit.score, hit.payload);
}
client.dispose();
```

Filter syntax is on [filtering](/vectors/filtering); engine-level tuning through
`search_params` / `searchParams` is on [search tuning](/vectors/search-tuning).

## Retrieving, counting, and deleting points with the Aetherfy SDKs

| Operation | Python | JavaScript |
|---|---|---|
| Retrieve by id | `retrieve(collection_name, ids, with_payload=True, with_vectors=False) -> List[Dict]` | `retrieve(collectionName, ids, options: RetrieveOptions = {}) -> Promise<Point[]>` |
| Count | `count(collection_name, count_filter=None, exact=True) -> int` | `count(collectionName, options: CountOptions = {}) -> Promise<number>` |
| Delete | `delete(collection_name, points_selector) -> bool` | `delete(collectionName, pointsSelector) -> Promise<boolean>` |

Two traps live in this group of Aetherfy methods. First, `retrieve` returns
plain dictionaries in Python but typed `Point` objects in JavaScript. Second,
`count` defaults `exact` to `True` in Python and `false` in JavaScript — the
same nominal call computes its answer differently in the two languages, so pass
`exact` explicitly whenever the number matters.

`delete` accepts either a list of point ids or a filter object; a list is sent as
`points`, anything else as `filter`.

```python
import os

from aetherfy_vectors import AetherfyVectorsClient

client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

points = client.retrieve("articles", ids=[1, 2], with_payload=True, with_vectors=True)
for point in points:  # dicts in Python
    print(point["id"], point.get("payload"))

print(client.count("articles", exact=True))
print(client.count("articles", count_filter={"must": [{"key": "lang", "match": {"value": "en"}}]}, exact=True))

print(client.delete("articles", [1, 2]))
print(client.delete("articles", {"must": [{"key": "lang", "match": {"value": "fr"}}]}))
client.close()
```

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });

const points = await client.retrieve('articles', [1, 2], {
  withPayload: true,
  withVectors: true,
});
for (const point of points) {
  // typed Point objects in JavaScript
  console.log(point.id, point.payload);
}

console.log(await client.count('articles', { exact: true }));
console.log(
  await client.count('articles', {
    countFilter: { must: [{ key: 'lang', match: { value: 'en' } }] },
    exact: true,
  }),
);

console.log(await client.delete('articles', [1, 2]));
console.log(
  await client.delete('articles', { must: [{ key: 'lang', match: { value: 'fr' } }] }),
);
client.dispose();
```

## Scrolling a collection with the Aetherfy SDKs

`scroll` is one page; `scroll_iter` / `scrollIter` walks the whole collection for
you. Aetherfy accepts a batch size between 1 and 1000 — outside that range the
Python SDK raises `ValueError` and the JavaScript SDK raises `RangeError`, before
any request is sent.

| Operation | Python | JavaScript |
|---|---|---|
| One page | `scroll(collection_name, limit=10, offset=None, scroll_filter=None, with_payload=True, with_vectors=False) -> Dict` | `scroll(collectionName, options) -> Promise<ScrollResult>` |
| Iterate | `scroll_iter(collection_name, *, batch_size=256, scroll_filter=None, with_payload=True, with_vectors=False) -> Iterator[Dict]` | `async *scrollIter(collectionName, options: ScrollIterOptions = {}) -> AsyncGenerator<ScrollPoint>` |

The page result keys differ: Python returns `{"points", "next_page_offset"}`,
JavaScript returns `{points, nextPageOffset}`. `scroll_iter` is **keyword-only**
in Python — `batch_size` cannot be passed positionally.

```python
import os

from aetherfy_vectors import AetherfyVectorsClient

client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

page = client.scroll("articles", limit=100, with_payload=True)
print(len(page["points"]), page["next_page_offset"])

for point in client.scroll_iter("articles", batch_size=256, with_payload=True):
    print(point["id"])
client.close()
```

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });

const page = await client.scroll('articles', { limit: 100, withPayload: true });
console.log(page.points.length, page.nextPageOffset);

for await (const point of client.scrollIter('articles', {
  batchSize: 256,
  withPayload: true,
})) {
  console.log(point.id);
}
client.dispose();
```

## Mutating payloads with the Aetherfy SDKs

Aetherfy exposes three payload mutations, and every one of them is capped at
**512 points per call** in both SDKs.

| Operation | Python | JavaScript | Effect |
|---|---|---|---|
| Set (merge) | `set_payload(collection_name, payload, points, key=None)` | `setPayload(collectionName, payload, points, options = {})` | Adds or overwrites the named keys; other keys survive |
| Overwrite (replace) | `overwrite_payload(collection_name, payload, points)` | `overwritePayload(collectionName, payload, points)` | Replaces the whole payload |
| Delete keys | `delete_payload(collection_name, keys, points)` | `deletePayload(collectionName, keys, points)` | Removes only the named keys |

Payload keys beginning `__aetherfy_` are reserved: Aetherfy rejects them on write
with 400 `RESERVED_FIELD` and strips them from reads.

```python
import os

from aetherfy_vectors import AetherfyVectorsClient

client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

client.set_payload("articles", payload={"reviewed": True}, points=[1, 2])
client.overwrite_payload("articles", payload={"lang": "en"}, points=[1])
client.delete_payload("articles", keys=["reviewed"], points=[1, 2])
client.close()
```

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });

await client.setPayload('articles', { reviewed: true }, [1, 2]);
await client.overwritePayload('articles', { lang: 'en' }, [1]);
await client.deletePayload('articles', ['reviewed'], [1, 2]);
client.dispose();
```

## Managing payload schemas with the Aetherfy SDKs

Aetherfy can hold a payload schema per collection and enforce it at one of three
levels.

| Operation | Python | JavaScript |
|---|---|---|
| Read | `get_schema(name)` | `getSchema(name)` |
| Write | `set_schema(name, schema, enforcement="off", description=None) -> etag` | `setSchema(name, schema, enforcementMode='off', description?)` |
| Delete | `delete_schema(name)` | `deleteSchema(name)` |
| Infer from data | `analyze_schema(collection_name, sample_size=1000)` | `analyzeSchema(collectionName, sampleSize = 1000)` |

| Enforcement mode | Behaviour |
|---|---|
| `off` | Schema stored, not enforced |
| `warn` | Violations reported, writes proceed |
| `strict` | Violations reject the write |

`set_schema` returns the new ETag, which is what you pass to the REST layer's
`If-Match` for a compare-and-set update. `analyze_schema` samples between 100 and
10 000 points; outside that range the call raises before any Aetherfy request.
A collection with no schema raises `SchemaNotFoundError`, and a write that
violates a `strict` schema raises `SchemaValidationError` (Python's carries
`.errors`).

```python
import os

from aetherfy_vectors import AetherfyVectorsClient, SchemaNotFoundError

client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

analysis = client.analyze_schema("articles", sample_size=500)
print(analysis)

try:
    print(client.get_schema("articles"))
except SchemaNotFoundError:
    print("no schema defined for this collection")
client.close()
```

```typescript
import { AetherfyVectorsClient, SchemaNotFoundError } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });

const analysis = await client.analyzeSchema('articles', 500);
console.log(analysis);

try {
  console.log(await client.getSchema('articles'));
} catch (err) {
  if (err instanceof SchemaNotFoundError) {
    console.log('no schema defined for this collection');
  } else {
    throw err;
  }
}
client.dispose();
```

## Point ID rules in the Aetherfy SDKs

Both Aetherfy SDKs validate point ids client-side, identically, before any
request leaves the process.

| Accepted | Notes |
|---|---|
| Unsigned integer | `0 <= id <= 2**53 - 1` |
| UUID string | Canonical, 32-hex simple, braced, or `urn:uuid:` form; case-insensitive |

Anything else raises `ValidationError` with the message:
`Point ID '<id>' is invalid — use an unsigned integer or a UUID string.`

The upper bound is 2^53 − 1 because the server's JSON layer parses numbers as
IEEE-754 doubles; a larger integer could not survive the round trip intact.
In Python, `bool` is explicitly rejected even though it subclasses `int`.

Aetherfy performs **no coercion in either direction**: an integer id comes back
an integer, and a UUID comes back a string. `1` and `"1"` are not
interchangeable, and `"1"` is not a valid id at all.

```python
import os

from aetherfy_vectors import AetherfyVectorsClient, Point

client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

client.upsert(
    "articles",
    [
        Point(id=42, vector=[0.1, 0.2, 0.3, 0.4]),
        Point(id="6f8b9a1c-2d3e-4f50-8a1b-2c3d4e5f6071", vector=[0.4, 0.3, 0.2, 0.1]),
    ],
)

fetched = client.retrieve("articles", ids=[42, "6f8b9a1c-2d3e-4f50-8a1b-2c3d4e5f6071"])
for point in fetched:
    print(type(point["id"]).__name__, point["id"])
client.close()
```

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });

await client.upsert('articles', [
  { id: 42, vector: [0.1, 0.2, 0.3, 0.4] },
  { id: '6f8b9a1c-2d3e-4f50-8a1b-2c3d4e5f6071', vector: [0.4, 0.3, 0.2, 0.1] },
]);

const fetched = await client.retrieve('articles', [
  42,
  '6f8b9a1c-2d3e-4f50-8a1b-2c3d4e5f6071',
]);
for (const point of fetched) {
  console.log(typeof point.id, point.id);
}
client.dispose();
```

## Model shapes in the Aetherfy SDKs

The Aetherfy Python SDK models are dataclasses; the JavaScript models are
interfaces. Field names differ where the language convention differs.

| Model | Python | JavaScript |
|---|---|---|
| Point | `Point(id: Union[str, int], vector: List[float], payload: Optional[Dict] = None)` | `Point{id, vector, payload?}` |
| Search hit | `SearchResult(id, score: float, payload=None, vector=None)` | `SearchResult{id, score, payload?, vector?}` |
| Vector config | `VectorConfig(size: int, distance: DistanceMetric)` | `VectorConfig{size, distance}` |
| Collection | `Collection(name, config, description=None, points_count=None, status=None, regions=None)` | `Collection{name, description?, config, pointsCount?, status?, regions?}` |
| Filter | `Filter(must=None, must_not=None, should=None)` | `Filter` — see [filtering](/vectors/filtering) |
| Scroll point | not a distinct type | `ScrollPoint{id, vector?, payload?}` |
| Scroll page | plain dict `{"points", "next_page_offset"}` | `ScrollResult{points, nextPageOffset}` |

`DistanceMetric` has the same four members in both Aetherfy SDKs. The value you
send and the value Aetherfy stores are not always the same string:

| Member | Wire value sent | Value Aetherfy stores and returns |
|---|---|---|
| `COSINE` | `Cosine` | `Cosine` |
| `EUCLIDEAN` | `Euclidean` | **`Euclid`** |
| `DOT` | `Dot` | `Dot` |
| `MANHATTAN` | `Manhattan` | `Manhattan` |

`EUCLIDEAN` does **not** round-trip. Aetherfy normalises distance names
case-insensitively and stores both `euclid` and `euclidean` as `Euclid`, so a
collection created with `DistanceMetric.EUCLIDEAN` reads back
`config.distance == "Euclid"` — an equality check against `'Euclidean'`
silently fails. `Cosine`, `Dot` and `Manhattan` round-trip unchanged.

## Managing an Aetherfy client's lifetime

The Aetherfy Python client supports the context-manager protocol and an explicit
`close()`. The Aetherfy JavaScript client has `dispose()` and `destroy()`, plus a
`testConnection()` probe that the Python SDK does not have.

```python
import os

from aetherfy_vectors import AetherfyVectorsClient

# Context manager closes the client on exit.
with AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"]) as client:
    print(client.get_collections())

# Or close it yourself.
client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])
try:
    print(client.collection_exists("articles"))
finally:
    client.close()
```

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });
try {
  console.log(await client.testConnection());
  console.log(await client.getCollections());
} finally {
  client.dispose();
}
```

## Divergences between the two Aetherfy SDKs

These are the differences that change behaviour, not just spelling. Read this
table before porting code between the two Aetherfy SDKs.

| Concern | Python | JavaScript |
|---|---|---|
| `count()` default `exact` | `True` | `false` |
| Timeout unit | seconds (`30.0`) | milliseconds (`30000`) |
| Scroll page keys | `{"points", "next_page_offset"}` | `{points, nextPageOffset}` |
| Collection point count field | `points_count` | `pointsCount` |
| `retrieve` return type | `List[Dict]` | `Point[]` |
| Base exception class | `AetherfyVectorsException` | `AetherfyVectorsError` |
| Call style | synchronous | `async` / `await` |
| Region pinning | `api_region` on the constructor | `apiRegion` requires `await Client.create()`; `new` throws |
| Lifecycle | context manager, `close()` | `dispose()`, `destroy()` |
| Connection probe | none | `testConnection()` |
| Batch-size range error | `ValueError` | `RangeError` |
| Unknown search option | native `TypeError` (no `**kwargs` sink) | explicit runtime guard throws |
| Memory layer import | `import aetherfy_memory` | package root only; no `aetherfy-vectors/memory` subpath |
| Payload mutation cap | 512 points per call | 512 points per call |

## Complete Aetherfy SDK method map

| Operation | Python | JavaScript |
|---|---|---|
| Create collection | `create_collection` | `createCollection` |
| Delete collection | `delete_collection` | `deleteCollection` |
| List collections | `get_collections` | `getCollections` |
| Collection exists | `collection_exists` | `collectionExists` |
| Get collection | `get_collection` | `getCollection` |
| Upsert | `upsert` | `upsert` |
| Search | `search` | `search` |
| Retrieve | `retrieve` | `retrieve` |
| Delete points | `delete` | `delete` |
| Count | `count` | `count` |
| Scroll one page | `scroll` | `scroll` |
| Scroll iterator | `scroll_iter` | `scrollIter` |
| Set payload | `set_payload` | `setPayload` |
| Overwrite payload | `overwrite_payload` | `overwritePayload` |
| Delete payload keys | `delete_payload` | `deletePayload` |
| Get schema | `get_schema` | `getSchema` |
| Set schema | `set_schema` | `setSchema` |
| Delete schema | `delete_schema` | `deleteSchema` |
| Analyze schema | `analyze_schema` | `analyzeSchema` |
| Close | `close` / `with` | `dispose` / `destroy` |
| Connection probe | — | `testConnection` |

The exception classes each Aetherfy SDK raises, and the error codes behind them,
are on [errors](/vectors/errors).
