diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index bfc26f9c..b6fd98c0 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "2.2.0"
+ ".": "2.3.0-alpha.1"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index cc6bfd14..74337dcd 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 14
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/turbopuffer-benesch/turbopuffer-6717a82f2b22fd2b55bce7acb7d5e9a694c51132a5522626d39684bff6e0cb83.yml
-openapi_spec_hash: 647e6105fbcaba7eaed15f1859f1c463
-config_hash: 3917af929e17def75be204eb7e4000fa
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/turbopuffer-benesch/turbopuffer-ffcaae9b05de6560eb11bb0821cf4d6ebc06f1464c6d8258d79c1a0f2eda41fb.yml
+openapi_spec_hash: 2ffbfcbe9c95eb73ef0be3dbef708e6a
+config_hash: bab72dc9f937352c7a01a37dadd44122
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4dff2c7..a40c3a32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
# Changelog
+## 2.3.0-alpha.1 (2026-06-02)
+
+Full Changelog: [v2.2.0...v2.3.0-alpha.1](https://github.com/turbopuffer/turbopuffer-python/compare/v2.2.0...v2.3.0-alpha.1)
+
+### Features
+
+* openapi: spec for `rerank_by: ["RRF"]` ([80ab218](https://github.com/turbopuffer/turbopuffer-python/commit/80ab218582e9b4e78dd9fca42cdce6f2a76761cf))
+* rename /docs/auth to /docs/overview ([1340eb1](https://github.com/turbopuffer/turbopuffer-python/commit/1340eb172500c78d9ce73bfe8ce557bcdb40c4d7))
+* spec: add SDK support for native embedding ([ca71729](https://github.com/turbopuffer/turbopuffer-python/commit/ca71729755294ed2eb073453e33baa8c89548063))
+
+
+### Bug Fixes
+
+* reject malicious poll locations ([#236](https://github.com/turbopuffer/turbopuffer-python/issues/236)) ([3f6123a](https://github.com/turbopuffer/turbopuffer-python/commit/3f6123a88c2c3795f9eb4da31ef66eaa9081db18))
+* type rerank_by parameter as RerankBy ([#239](https://github.com/turbopuffer/turbopuffer-python/issues/239)) ([6f07a95](https://github.com/turbopuffer/turbopuffer-python/commit/6f07a95860f5dff9b699776d7916606b5c6f7cab))
+
+
+### Chores
+
+* fix API docs links ([#235](https://github.com/turbopuffer/turbopuffer-python/issues/235)) ([204b46b](https://github.com/turbopuffer/turbopuffer-python/commit/204b46bf15c5694bdd38339b8471c46e16e697a0))
+
## 2.2.0 (2026-05-28)
Full Changelog: [v2.1.0...v2.2.0](https://github.com/turbopuffer/turbopuffer-python/compare/v2.1.0...v2.2.0)
diff --git a/README.md b/README.md
index 2db55f3a..70804615 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
-The turbopuffer Python library provides convenient access to the Turbopuffer HTTP API from any Python 3.9+
+The turbopuffer Python library provides convenient access to the [turbopuffer HTTP API](https://turbopuffer.com/docs/overview) from any Python 3.9+
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
@@ -11,7 +11,7 @@ It is generated with [Stainless](https://www.stainless.com/).
## MCP Server
-Use the Turbopuffer MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
+Use the turbopuffer MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
[](https://cursor.com/en-US/install-mcp?name=%40turbopuffer%2Fturbopuffer-mcp&config=eyJuYW1lIjoiQHR1cmJvcHVmZmVyL3R1cmJvcHVmZmVyLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL3R1cmJvcHVmZmVyLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtdHVyYm9wdWZmZXItYXBpLWtleSI6InRwdWZfQTEuLi4ifX0)
[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40turbopuffer%2Fturbopuffer-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fturbopuffer.stlmcp.com%22%2C%22headers%22%3A%7B%22x-turbopuffer-api-key%22%3A%22tpuf_A1...%22%7D%7D)
@@ -20,13 +20,13 @@ Use the Turbopuffer MCP Server to enable AI assistants to interact with this API
## Documentation
-The HTTP API documentation can be found at [turbopuffer.com/docs](https://turbopuffer.com/docs).
+The HTTP API documentation can be found at [turbopuffer.com/docs/overview](https://turbopuffer.com/docs/overview).
## Installation
```sh
# install from PyPI
-pip install turbopuffer
+pip install --pre turbopuffer
```
## Usage
@@ -128,7 +128,7 @@ You can enable this by installing `aiohttp`:
```sh
# install from PyPI
-pip install turbopuffer[aiohttp]
+pip install --pre turbopuffer[aiohttp]
```
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
@@ -172,7 +172,7 @@ Typed requests and responses provide autocomplete and documentation within your
## Pagination
-List methods in the Turbopuffer API are paginated.
+List methods in the turbopuffer API are paginated.
This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
diff --git a/api.md b/api.md
index cdce2855..8f6d7777 100644
--- a/api.md
+++ b/api.md
@@ -18,6 +18,8 @@ Types:
from turbopuffer.types import (
AggregateBy,
AggregationGroup,
+ AttributeEmbed,
+ AttributeEmbedConfig,
AttributeSchema,
AttributeSchemaConfig,
AttributeType,
@@ -29,6 +31,7 @@ from turbopuffer.types import (
CopyFromNamespaceParams,
DecayParams,
DistanceMetric,
+ EmbedParams,
Encryption,
FullTextSearch,
FullTextSearchConfig,
@@ -44,6 +47,7 @@ from turbopuffer.types import (
QueryBilling,
QueryPerformance,
Row,
+ RrfParams,
SaturateParams,
SparseDistanceMetric,
Tokenizer,
diff --git a/pyproject.toml b/pyproject.toml
index c819bb1e..c9861379 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "turbopuffer"
-version = "2.2.0"
+version = "2.3.0-alpha.1"
description = "The official Python library for the turbopuffer API"
dynamic = ["readme"]
license = "MIT"
diff --git a/scripts/gen b/scripts/gen
index 23fecb84..a0e66a12 100755
--- a/scripts/gen
+++ b/scripts/gen
@@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")/.."
-apigen_image=ghcr.io/turbopuffer/turbopuffer-apigen:864b25e7f396607f5fee302f6aed713afab55020
+apigen_image=ghcr.io/turbopuffer/turbopuffer-apigen:1b58f2aa9172bf7a668bf862c271b852e95a846b
apigen() {
if [[ "$TURBOPUFFER_DEV_APIGEN" ]]; then
diff --git a/src/turbopuffer/_version.py b/src/turbopuffer/_version.py
index e9a56e32..e12dcfdc 100644
--- a/src/turbopuffer/_version.py
+++ b/src/turbopuffer/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "turbopuffer"
-__version__ = "2.2.0" # x-release-please-version
+__version__ = "2.3.0-alpha.1" # x-release-please-version
diff --git a/src/turbopuffer/lib/respond_async.py b/src/turbopuffer/lib/respond_async.py
index a39684e8..f0c3769d 100644
--- a/src/turbopuffer/lib/respond_async.py
+++ b/src/turbopuffer/lib/respond_async.py
@@ -118,14 +118,33 @@ def _respond_async_applied(response: httpx.Response) -> bool:
def _extract_location(response: httpx.Response) -> str:
- location: str = response.headers.get(HEADER_LOCATION, "").strip()
- if not location:
+ raw_location: str = response.headers.get(HEADER_LOCATION, "").strip()
+ if not raw_location:
raise APIResponseValidationError(
response=response,
body=response.text,
message="missing 'Location' header on respond-async response",
)
- return location
+
+ orig = response.request.url
+ try:
+ # Resolve the Location against the original request URL.
+ location = orig.join(raw_location)
+ except httpx.InvalidURL as err:
+ raise APIResponseValidationError(
+ response=response,
+ body=response.text,
+ message=f"malformed 'Location' header: {raw_location!r}",
+ ) from err
+
+ # Reject a Location pointing at a different origin, to prevent API key exfiltration.
+ if (location.scheme, location.host, location.port) != (orig.scheme, orig.host, orig.port):
+ raise APIResponseValidationError(
+ response=response,
+ body=response.text,
+ message=f"'Location' origin does not match request origin: {raw_location!r}",
+ )
+ return str(location)
class _PollError(BaseModel):
diff --git a/src/turbopuffer/resources/namespaces.py b/src/turbopuffer/resources/namespaces.py
index 4f0d1098..876edec2 100644
--- a/src/turbopuffer/resources/namespaces.py
+++ b/src/turbopuffer/resources/namespaces.py
@@ -33,7 +33,7 @@
)
from .._exceptions import NotFoundError
from .._base_client import make_request_options
-from ..types.custom import Filter, RankBy, GroupBy, AggregateBy
+from ..types.custom import Filter, RankBy, GroupBy, RerankBy, AggregateBy
from ..types.id_param import IDParam
from ..types.row_param import RowParam
from ..types.columns_param import ColumnsParam
@@ -376,6 +376,7 @@ def multi_query(
namespace: str | None = None,
queries: Iterable[namespace_multi_query_params.Query],
consistency: namespace_multi_query_params.Consistency | Omit = omit,
+ rerank_by: RerankBy | Omit = omit,
vector_encoding: VectorEncoding | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -390,6 +391,8 @@ def multi_query(
Args:
consistency: The consistency level for a query.
+ rerank_by: How to combine the rows returned by each sub-query into a single ranked list.
+
vector_encoding: The encoding to use for vectors in the response.
extra_headers: Send extra headers
@@ -410,6 +413,7 @@ def multi_query(
{
"queries": queries,
"consistency": consistency,
+ "rerank_by": rerank_by,
"vector_encoding": vector_encoding,
},
namespace_multi_query_params.NamespaceMultiQueryParams,
@@ -1131,6 +1135,7 @@ async def multi_query(
namespace: str | None = None,
queries: Iterable[namespace_multi_query_params.Query],
consistency: namespace_multi_query_params.Consistency | Omit = omit,
+ rerank_by: RerankBy | Omit = omit,
vector_encoding: VectorEncoding | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -1145,6 +1150,8 @@ async def multi_query(
Args:
consistency: The consistency level for a query.
+ rerank_by: How to combine the rows returned by each sub-query into a single ranked list.
+
vector_encoding: The encoding to use for vectors in the response.
extra_headers: Send extra headers
@@ -1165,6 +1172,7 @@ async def multi_query(
{
"queries": queries,
"consistency": consistency,
+ "rerank_by": rerank_by,
"vector_encoding": vector_encoding,
},
namespace_multi_query_params.NamespaceMultiQueryParams,
diff --git a/src/turbopuffer/types/__init__.py b/src/turbopuffer/types/__init__.py
index 421e358c..79a6a0d8 100644
--- a/src/turbopuffer/types/__init__.py
+++ b/src/turbopuffer/types/__init__.py
@@ -11,8 +11,10 @@
from .row_param import RowParam as RowParam
from .tokenizer import Tokenizer as Tokenizer
from .encryption import Encryption as Encryption
+from .rrf_params import RrfParams as RrfParams
from .limit_param import LimitParam as LimitParam
from .decay_params import DecayParams as DecayParams
+from .embed_params import EmbedParams as EmbedParams
from .fuzzy_params import FuzzyParams as FuzzyParams
from .vector_param import VectorParam as VectorParam
from .columns_param import ColumnsParam as ColumnsParam
@@ -20,6 +22,7 @@
from .write_billing import WriteBilling as WriteBilling
from .attribute_type import AttributeType as AttributeType
from .pinning_config import PinningConfig as PinningConfig
+from .attribute_embed import AttributeEmbed as AttributeEmbed
from .distance_metric import DistanceMetric as DistanceMetric
from .saturate_params import SaturateParams as SaturateParams
from .vector_encoding import VectorEncoding as VectorEncoding
@@ -32,6 +35,8 @@
from .bm25_clause_params import Bm25ClauseParams as Bm25ClauseParams
from .namespace_metadata import NamespaceMetadata as NamespaceMetadata
from .pinning_config_param import PinningConfigParam as PinningConfigParam
+from .attribute_embed_param import AttributeEmbedParam as AttributeEmbedParam
+from .attribute_embed_config import AttributeEmbedConfig as AttributeEmbedConfig
from .attribute_schema_param import AttributeSchemaParam as AttributeSchemaParam
from .full_text_search_param import FullTextSearchParam as FullTextSearchParam
from .namespace_query_params import NamespaceQueryParams as NamespaceQueryParams
@@ -48,6 +53,7 @@
from .namespace_schema_response import NamespaceSchemaResponse as NamespaceSchemaResponse
from .copy_from_namespace_params import CopyFromNamespaceParams as CopyFromNamespaceParams
from .namespace_copy_from_params import NamespaceCopyFromParams as NamespaceCopyFromParams
+from .attribute_embed_config_param import AttributeEmbedConfigParam as AttributeEmbedConfigParam
from .branch_from_namespace_params import BranchFromNamespaceParams as BranchFromNamespaceParams
from .namespace_branch_from_params import NamespaceBranchFromParams as NamespaceBranchFromParams
from .namespace_copy_from_response import NamespaceCopyFromResponse as NamespaceCopyFromResponse
diff --git a/src/turbopuffer/types/attribute_embed.py b/src/turbopuffer/types/attribute_embed.py
new file mode 100644
index 00000000..ca8e8b3d
--- /dev/null
+++ b/src/turbopuffer/types/attribute_embed.py
@@ -0,0 +1,10 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union
+from typing_extensions import TypeAlias
+
+from .attribute_embed_config import AttributeEmbedConfig
+
+__all__ = ["AttributeEmbed"]
+
+AttributeEmbed: TypeAlias = Union[str, AttributeEmbedConfig, None]
diff --git a/src/turbopuffer/types/attribute_embed_config.py b/src/turbopuffer/types/attribute_embed_config.py
new file mode 100644
index 00000000..d6b8338b
--- /dev/null
+++ b/src/turbopuffer/types/attribute_embed_config.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+
+__all__ = ["AttributeEmbedConfig"]
+
+
+class AttributeEmbedConfig(BaseModel):
+ """Configuration options for automatic embedding."""
+
+ model: str
+ """The model to use for embedding.
+
+ See our documentation for a list of models supported in each region.
+ """
+
+ attribute: Optional[str] = None
+ """The name of an existing vector attribute to store embeddings in.
+
+ If omitted, turbopuffer will generate a computed vector attribute named
+ `$embed_`.
+ """
+
+ dims: Optional[int] = None
+ """The dimensionality to embed at.
+
+ If not set, will pick the default for this model. If you're storing embeddings
+ in an existing attribute, this can be omitted, and may not be set to a value
+ other than the dimensions of that attribute.
+ """
diff --git a/src/turbopuffer/types/attribute_embed_config_param.py b/src/turbopuffer/types/attribute_embed_config_param.py
new file mode 100644
index 00000000..1ff2ce59
--- /dev/null
+++ b/src/turbopuffer/types/attribute_embed_config_param.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["AttributeEmbedConfigParam"]
+
+
+class AttributeEmbedConfigParam(TypedDict, total=False):
+ """Configuration options for automatic embedding."""
+
+ model: Required[str]
+ """The model to use for embedding.
+
+ See our documentation for a list of models supported in each region.
+ """
+
+ attribute: str
+ """The name of an existing vector attribute to store embeddings in.
+
+ If omitted, turbopuffer will generate a computed vector attribute named
+ `$embed_`.
+ """
+
+ dims: int
+ """The dimensionality to embed at.
+
+ If not set, will pick the default for this model. If you're storing embeddings
+ in an existing attribute, this can be omitted, and may not be set to a value
+ other than the dimensions of that attribute.
+ """
diff --git a/src/turbopuffer/types/attribute_embed_param.py b/src/turbopuffer/types/attribute_embed_param.py
new file mode 100644
index 00000000..fc65d093
--- /dev/null
+++ b/src/turbopuffer/types/attribute_embed_param.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from typing_extensions import TypeAlias
+
+from .attribute_embed_config_param import AttributeEmbedConfigParam
+
+__all__ = ["AttributeEmbedParam"]
+
+AttributeEmbedParam: TypeAlias = Union[str, AttributeEmbedConfigParam]
diff --git a/src/turbopuffer/types/attribute_schema_config.py b/src/turbopuffer/types/attribute_schema_config.py
index 08caff3f..10f437c1 100644
--- a/src/turbopuffer/types/attribute_schema_config.py
+++ b/src/turbopuffer/types/attribute_schema_config.py
@@ -5,6 +5,7 @@
from .._models import BaseModel
from .attribute_type import AttributeType
+from .attribute_embed import AttributeEmbed
from .distance_metric import DistanceMetric
from .full_text_search import FullTextSearch
from .sparse_distance_metric import SparseDistanceMetric
@@ -48,6 +49,13 @@ class AttributeSchemaConfig(BaseModel):
Can be a boolean or a detailed configuration object.
"""
+ embed: Optional[AttributeEmbed] = None
+ """Whether to automatically embed this string attribute into a vector attribute.
+
+ Can be a model name, a detailed configuration object, or `null` to remove an
+ existing embedding configuration.
+ """
+
filterable: Optional[bool] = None
"""Whether or not the attributes can be used in filters."""
diff --git a/src/turbopuffer/types/attribute_schema_config_param.py b/src/turbopuffer/types/attribute_schema_config_param.py
index d321003f..ab1d06d5 100644
--- a/src/turbopuffer/types/attribute_schema_config_param.py
+++ b/src/turbopuffer/types/attribute_schema_config_param.py
@@ -2,11 +2,12 @@
from __future__ import annotations
-from typing import Union
+from typing import Union, Optional
from typing_extensions import Required, TypeAlias, TypedDict
from .attribute_type import AttributeType
from .distance_metric import DistanceMetric
+from .attribute_embed_param import AttributeEmbedParam
from .full_text_search_param import FullTextSearchParam
from .sparse_distance_metric import SparseDistanceMetric
@@ -49,6 +50,13 @@ class AttributeSchemaConfigParam(TypedDict, total=False):
Can be a boolean or a detailed configuration object.
"""
+ embed: Optional[AttributeEmbedParam]
+ """Whether to automatically embed this string attribute into a vector attribute.
+
+ Can be a model name, a detailed configuration object, or `null` to remove an
+ existing embedding configuration.
+ """
+
filterable: bool
"""Whether or not the attributes can be used in filters."""
diff --git a/src/turbopuffer/types/custom.py b/src/turbopuffer/types/custom.py
index bd19564f..e1d9f2f8 100644
--- a/src/turbopuffer/types/custom.py
+++ b/src/turbopuffer/types/custom.py
@@ -2,7 +2,9 @@
from typing import Any, Tuple, Union, Literal, Mapping, Sequence, TypedDict
+from .rrf_params import RrfParams
from .decay_params import DecayParams
+from .embed_params import EmbedParams
from .fuzzy_params import FuzzyParams
from .saturate_params import SaturateParams
from .bm25_clause_params import Bm25ClauseParams
@@ -11,7 +13,7 @@
AggregateBy = Union[Tuple[Literal["Count"]], Tuple[Literal["Sum"], str], Tuple[Literal["Count"], str]]
ExprRefNew = TypedDict("ExprRefNew", {"$ref_new": str})
-Expr = ExprRefNew
+Expr = Union[ExprRefNew, Tuple[Literal["Embed"], str], Tuple[Literal["Embed"], str, EmbedParams]]
Filter = Union[
Tuple[str, Literal["Eq"], Any],
Tuple[str, Literal["NotEq"], Any],
@@ -52,7 +54,9 @@
GroupByFunction = Tuple[Literal["ForEachUnique"], str]
GroupBy = Union[str, Mapping[str, GroupByFunction]]
RankByAnn = Tuple[str, Literal["ANN"], Sequence[float]]
+RankByAnnExpr = Tuple[str, Literal["ANN"], Expr]
RankByKnn = Tuple[str, Literal["kNN"], Sequence[float]]
+RankByKnnExpr = Tuple[str, Literal["kNN"], Expr]
RankBySparseKnn = Tuple[str, Literal["SparseKNN"], Mapping[str, float]]
RankByText = Union[
Tuple[str, Literal["BM25"], str],
@@ -74,9 +78,12 @@
RankByAttributes = Sequence[RankByAttribute]
RankBy = Union[
RankByAnn,
+ RankByAnnExpr,
RankByKnn,
+ RankByKnnExpr,
RankBySparseKnn,
RankByText,
RankByAttribute,
RankByAttributes,
]
+RerankBy = Union[Tuple[Literal["RRF"]], Tuple[Literal["RRF"], RrfParams]]
diff --git a/src/turbopuffer/types/embed_params.py b/src/turbopuffer/types/embed_params.py
new file mode 100644
index 00000000..d928a278
--- /dev/null
+++ b/src/turbopuffer/types/embed_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["EmbedParams"]
+
+
+class EmbedParams(TypedDict, total=False):
+ """Additional (optional) parameters for the Embed expression."""
+
+ model: str
+ """
+ The model to use for embedding, overriding the model configured for the
+ attribute.
+ """
diff --git a/src/turbopuffer/types/namespace_multi_query_params.py b/src/turbopuffer/types/namespace_multi_query_params.py
index 4dac2969..140fd46b 100644
--- a/src/turbopuffer/types/namespace_multi_query_params.py
+++ b/src/turbopuffer/types/namespace_multi_query_params.py
@@ -23,6 +23,9 @@ class NamespaceMultiQueryParams(TypedDict, total=False):
consistency: Consistency
"""The consistency level for a query."""
+ rerank_by: object
+ """How to combine the rows returned by each sub-query into a single ranked list."""
+
vector_encoding: VectorEncoding
"""The encoding to use for vectors in the response."""
diff --git a/src/turbopuffer/types/rrf_params.py b/src/turbopuffer/types/rrf_params.py
new file mode 100644
index 00000000..3e6c0c1a
--- /dev/null
+++ b/src/turbopuffer/types/rrf_params.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["RrfParams"]
+
+
+class RrfParams(TypedDict, total=False):
+ """Configuration options for RRF."""
+
+ rank_constant: int
+ """RRF rank constant (`k`). Must be greater than zero. Defaults to `60`."""
diff --git a/tests/api_resources/test_namespaces.py b/tests/api_resources/test_namespaces.py
index 610e50d4..553f47c0 100644
--- a/tests/api_resources/test_namespaces.py
+++ b/tests/api_resources/test_namespaces.py
@@ -299,6 +299,7 @@ def test_method_multi_query_with_all_params(self, client: Turbopuffer) -> None:
}
],
consistency={"level": "strong"},
+ rerank_by=("RRF",),
vector_encoding="float",
)
assert_matches_type(NamespaceMultiQueryResponse, namespace, path=["response"])
@@ -917,6 +918,7 @@ async def test_method_multi_query_with_all_params(self, async_client: AsyncTurbo
}
],
consistency={"level": "strong"},
+ rerank_by=("RRF",),
vector_encoding="float",
)
assert_matches_type(NamespaceMultiQueryResponse, namespace, path=["response"])
diff --git a/tests/custom/test_respond_async.py b/tests/custom/test_respond_async.py
index f600c044..376f92e2 100644
--- a/tests/custom/test_respond_async.py
+++ b/tests/custom/test_respond_async.py
@@ -360,6 +360,45 @@ async def test_async_async_applied_missing_location_header() -> None:
await client.namespace("test").write(upsert_columns={"id": [1], "vector": [[0.1]]})
+BAD_LOCATIONS = [
+ "https://evil.example.com/v1/ops/op-x",
+ "//evil.example.com/v1/ops/op-x",
+ "http://api.turbopuffer.com/v1/ops/op-x",
+ "http://host:notaport/x",
+]
+
+
+@respx.mock
+@pytest.mark.parametrize("bad_location", BAD_LOCATIONS)
+def test_sync_async_applied_bad_location(bad_location: str) -> None:
+ respx.post(f"{base_url}/v2/namespaces/test").mock(
+ return_value=httpx.Response(
+ 202,
+ headers={"preference-applied": "respond-async", "location": bad_location},
+ )
+ )
+ http_client = httpx.Client(transport=httpx.HTTPTransport())
+ client = Turbopuffer(base_url=base_url, api_key=api_key, http_client=http_client)
+ with pytest.raises(turbopuffer.APIResponseValidationError):
+ client.namespace("test").write(upsert_columns={"id": [1], "vector": [[0.1]]})
+
+
+@respx.mock
+@pytest.mark.asyncio
+@pytest.mark.parametrize("bad_location", BAD_LOCATIONS)
+async def test_async_async_applied_bad_location(bad_location: str) -> None:
+ respx.post(f"{base_url}/v2/namespaces/test").mock(
+ return_value=httpx.Response(
+ 202,
+ headers={"preference-applied": "respond-async", "location": bad_location},
+ )
+ )
+ http_client = httpx.AsyncClient(transport=httpx.AsyncHTTPTransport())
+ async with AsyncTurbopuffer(base_url=base_url, api_key=api_key, http_client=http_client) as client:
+ with pytest.raises(turbopuffer.APIResponseValidationError):
+ await client.namespace("test").write(upsert_columns={"id": [1], "vector": [[0.1]]})
+
+
@respx.mock
def test_sync_poll_too_many_failures() -> None:
poll_url = f"{base_url}/v1/namespaces/test/operations/op-dead"