Skip to content

Commit 75c452e

Browse files
Add 0090 Cohere /v2/rerank wire mapping (#215)
* Add 0090 Cohere /v2/rerank wire mapping Formalize the retrieval-provider 0090 Cohere /v2/rerank wire mapping (section 8.4) over the existing CohereRerankProvider from 0060. The provider already implements the wire: {model, query, documents (string array), top_n from top_k}, return_documents a silent no-op (the Cohere wire has no such field), max_tokens_per_doc via the extras bag, and search_units usage. base_url now defaults to https://api.cohere.com per section 8.4 (was a required arg). Un-defer conformance fixtures 028-031, read a cohere_rerank_provider block in the harness, and flip conformance.toml. Env-gated live tests cover the rerank plus the return_documents no-op. * Close the base_url construction-guard test clients Make the two CohereRerankProvider base_url construction-guard tests async and await provider.aclose() after each successful construction, so the underlying httpx.AsyncClient is not left open (a ResourceWarning under strict runs). The /v2-suffix rejection raises before a client is created, so only the accepted constructions need closing.
1 parent 286ebe8 commit 75c452e

5 files changed

Lines changed: 127 additions & 22 deletions

File tree

conformance.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -867,10 +867,11 @@ note = "The embedding half shipped in 0059b (#198): EmbeddingEvent.output_vector
867867

868868
# Spec v0.85.0 (proposal 0090). Cohere rerank wire mapping
869869
# (retrieval-provider §8.4, POST /v2/rerank; backs 0060's Cohere-shaped
870-
# reference reranker). Not-yet; the rerank capability (0060) is
871-
# unshipped, so its wire fixtures 028-031 defer with it.
870+
# reference reranker).
872871
[proposals."0090"]
873-
status = "not-yet"
872+
status = "implemented"
873+
since = "0.16.0"
874+
note = "Retrieval-provider Cohere /v2/rerank wire mapping (§8.4) -- formalizes the wire the CohereRerankProvider reference reranker (shipped in 0060) already implements. POST {base_url}/v2/rerank with {model, query, documents (string array), top_n from top_k (omitted when None)}; documents sent as the string-array form (Cohere v2 takes strings only -- no per-document object / rank_fields). return_documents is NOT realized: the /v2/rerank wire has no such field and never echoes document text, so it is a silent no-op (ScoredDocument.document stays null on every result regardless of the config value, and the mapping does not error) -- the §8.3 'knob with no wire to land on' pattern. max_tokens_per_doc / truncation has no fail-loud option (Cohere truncates server-side to its 4096 default), so unlike §8.1 TEI / §8.2 Jina no truncate flag is sent; max_tokens_per_doc rides the extras-pass-through bag. The response {id, results: [{index, relevance_score}], meta.billed_units.search_units} maps onto §6: index / relevance_score to ScoredDocument, meta.billed_units.search_units to RerankUsage.search_units (input_tokens null), id to response_id; Cohere echoes no model field, so RerankResponse.model is the bound model; results sorted descending with the §6 valid-index / no-duplicate / result-count invariants enforced. base_url defaults to https://api.cohere.com per §8.4 (was a required arg), sent with api_key as Authorization: Bearer. Conformance fixtures 028-031 pass via the general wire-capture harness (cohere_rerank_provider block; request-origin asserted against base_url). The error map + rerank observability ride 0060 / 0060b."
874875

875876
# Spec v0.86.0 (proposal 0091). Cohere embeddings wire mapping
876877
# (retrieval-provider §8.4, POST /v2/embed) extending §8.4 to both

src/openarmature/retrieval/providers/cohere.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,10 @@ def _request_params_from_config(config: RerankRuntimeConfig | None) -> dict[str,
108108
class CohereRerankProvider:
109109
"""Cohere ``/v2/rerank`` wire-shape rerank provider.
110110
111-
Construct with a base URL (host root), the bound rerank model, and an
112-
optional API key + transport. ``rerank()`` posts to ``/v2/rerank``.
111+
Construct with the bound rerank model and an optional API key +
112+
transport. ``base_url`` is the host root and defaults to the Cohere
113+
origin (``https://api.cohere.com``), overridable for a proxy / gateway.
114+
``rerank()`` posts to ``/v2/rerank``.
113115
114116
``ready()`` verifies the bound model with a minimal one-document
115117
``/v2/rerank`` probe. The Cohere ``/v2/rerank`` wire exposes no
@@ -120,7 +122,7 @@ class CohereRerankProvider:
120122
def __init__(
121123
self,
122124
*,
123-
base_url: str,
125+
base_url: str = "https://api.cohere.com",
124126
model: str,
125127
api_key: str | None = None,
126128
transport: httpx.AsyncBaseTransport | None = None,

tests/conformance/test_retrieval_provider.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,6 @@
8989
# 006-012; the 0090 Cohere wire-mapping fixtures 028-031 assert the request-
9090
# side wire contract, deferred until the Cohere wire-mapping impl PR.)
9191
_DEFERRED_FIXTURES: dict[str, str] = {
92-
**{
93-
p.stem: "Cohere rerank wire mapping (proposal 0090) not implemented"
94-
for p in CONFORMANCE_DIR.glob("[0-9][0-9][0-9]-*.yaml")
95-
if 28 <= int(p.stem[:3]) <= 31
96-
},
9792
**{
9893
p.stem: "Cohere embed wire mapping (proposal 0091) not implemented"
9994
for p in CONFORMANCE_DIR.glob("[0-9][0-9][0-9]-*.yaml")
@@ -238,8 +233,10 @@ def _build_rerank_provider(
238233
``mapping: tei`` builds a TeiRerankProvider from the suite-level
239234
``tei_rerank_provider`` block; ``mapping: jina`` builds a JinaRerankProvider
240235
from ``jina_rerank_provider`` (api_key from the block, sent as
241-
Authorization: Bearer); any other value (or absent) builds the default
242-
CohereRerankProvider. All drive the same MockTransport.
236+
Authorization: Bearer); any other value (or absent) builds a
237+
CohereRerankProvider, from the ``cohere_rerank_provider`` block
238+
(base_url / model / api_key) when the fixture declares one, else the
239+
default construction. All drive the same MockTransport.
243240
"""
244241
transport, captured = _build_handler(responses)
245242
if mapping == "tei":
@@ -260,12 +257,29 @@ def _build_rerank_provider(
260257
transport=transport,
261258
)
262259
else:
263-
provider = CohereRerankProvider(
264-
base_url="http://mock-rerank.test",
265-
model=model,
266-
api_key="test-key",
267-
transport=transport,
268-
)
260+
block = cast("Mapping[str, Any] | None", spec.get("cohere_rerank_provider"))
261+
if block is not None:
262+
# The §8.4 wire-mapping fixtures (028-031) declare the bound provider
263+
# in cohere_rerank_provider: base_url / model / api_key (sent as
264+
# Authorization: Bearer). base_url / api_key are read with defaults --
265+
# §8.4 makes base_url optional (the provider defaults to the Cohere
266+
# origin) and api_key optional (no auth header) -- so a block that
267+
# omits either binds the default rather than raising KeyError.
268+
provider = CohereRerankProvider(
269+
base_url=cast("str", block.get("base_url", "https://api.cohere.com")),
270+
model=cast("str", block.get("model", model)),
271+
api_key=cast("str | None", block.get("api_key")),
272+
transport=transport,
273+
)
274+
else:
275+
# The 0060 protocol fixtures (006-012, mapping absent) carry no
276+
# provider block; the default construction serves them.
277+
provider = CohereRerankProvider(
278+
base_url="http://mock-rerank.test",
279+
model=model,
280+
api_key="test-key",
281+
transport=transport,
282+
)
269283
return provider, captured
270284

271285

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Integration tests for the Cohere wire mappings against the live Cohere API.
2+
3+
Gated by the ``COHERE_API_KEY`` env var: the tests run only when it is set (a
4+
live Cohere API key). Skipped in CI and local runs that don't have it in scope;
5+
runs end-to-end against the hosted Cohere endpoint when invoked with the key set.
6+
7+
``base_url`` is read from ``COHERE_BASE_URL`` (default ``https://api.cohere.com``),
8+
and the bound rerank model from ``COHERE_RERANK_MODEL`` (a sensible Cohere
9+
default, overridable via env). Nothing is hardcoded to a specific deployment.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import os
15+
16+
import pytest
17+
18+
from openarmature.retrieval import CohereRerankProvider, RerankRuntimeConfig
19+
20+
_API_KEY = os.environ.get("COHERE_API_KEY")
21+
_BASE_URL = os.environ.get("COHERE_BASE_URL", "https://api.cohere.com")
22+
_RERANK_MODEL = os.environ.get("COHERE_RERANK_MODEL", "rerank-v3.5")
23+
24+
requires_cohere = pytest.mark.skipif(not _API_KEY, reason="Requires COHERE_API_KEY (live Cohere API key)")
25+
26+
27+
@pytest.mark.integration
28+
@requires_cohere
29+
async def test_cohere_rerank_returns_sorted_results_with_search_units() -> None:
30+
"""rerank() on a small pool returns results sorted by relevance descending,
31+
each index valid into the input documents, with a search_units usage record
32+
(Cohere meters rerank by search units, not tokens)."""
33+
documents = [
34+
"The Sea of Tranquility was the Apollo 11 landing site.",
35+
"Cheese is made from milk.",
36+
"The lunar south pole holds water ice in permanently shadowed craters.",
37+
]
38+
provider = CohereRerankProvider(model=_RERANK_MODEL, api_key=str(_API_KEY), base_url=_BASE_URL)
39+
try:
40+
response = await provider.rerank("Where on the moon is there water ice?", documents)
41+
finally:
42+
await provider.aclose()
43+
44+
scores = [r.relevance_score for r in response.results]
45+
assert scores == sorted(scores, reverse=True)
46+
assert all(0 <= r.index < len(documents) for r in response.results)
47+
assert len({r.index for r in response.results}) == len(response.results)
48+
# Cohere meters rerank by search_units -> search_units; input_tokens stays null.
49+
assert response.usage is not None
50+
assert response.usage.search_units is not None
51+
assert response.usage.input_tokens is None
52+
53+
54+
@pytest.mark.integration
55+
@requires_cohere
56+
async def test_cohere_rerank_return_documents_is_wire_noop() -> None:
57+
"""return_documents=True is a silent no-op on the Cohere wire: the results
58+
still come back (no error), and ScoredDocument.document is null on every
59+
result -- Cohere never echoes document text on /v2/rerank."""
60+
documents = [
61+
"The lunar maria are dark basaltic plains.",
62+
"Photosynthesis occurs in chloroplasts.",
63+
]
64+
provider = CohereRerankProvider(model=_RERANK_MODEL, api_key=str(_API_KEY), base_url=_BASE_URL)
65+
try:
66+
response = await provider.rerank(
67+
"What are the dark plains on the moon?",
68+
documents,
69+
config=RerankRuntimeConfig(return_documents=True),
70+
)
71+
finally:
72+
await provider.aclose()
73+
74+
assert len(response.results) == len(documents)
75+
assert all(r.document is None for r in response.results)

tests/unit/test_retrieval_provider.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -792,11 +792,24 @@ def handler(_req: httpx.Request) -> httpx.Response: # pragma: no cover - never
792792
# --- construction guards + readiness ----------------------------------------
793793

794794

795-
def test_rerank_base_url_rejects_v2_suffix() -> None:
795+
async def test_rerank_base_url_rejects_v2_suffix() -> None:
796796
with pytest.raises(ValueError, match="host root"):
797797
CohereRerankProvider(base_url="https://api.cohere.com/v2", model="m")
798-
# The host root is accepted (no doubled /v2/v2).
799-
CohereRerankProvider(base_url="https://api.cohere.com", model="m")
798+
# The host root is accepted (no doubled /v2/v2). The /v2-suffix rejection
799+
# above raises in normalize_base_url before a client is created, so only the
800+
# accepted construction here needs closing.
801+
provider = CohereRerankProvider(base_url="https://api.cohere.com", model="m")
802+
await provider.aclose()
803+
804+
805+
async def test_rerank_base_url_defaults_to_cohere_origin() -> None:
806+
# base_url is optional per §8.4: an unspecified base_url binds the Cohere
807+
# origin (host root; the provider appends /v2/rerank itself).
808+
provider = CohereRerankProvider(model="rerank-test")
809+
try:
810+
assert provider.base_url == "https://api.cohere.com"
811+
finally:
812+
await provider.aclose()
800813

801814

802815
async def test_rerank_ready_probe_surfaces_invalid_model_on_404() -> None:

0 commit comments

Comments
 (0)