Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ print(message.content)

### Response metadata

Use `with_response_metadata` to access the gateway's `X-Otari-Request-ID`:
Use `with_response_metadata` to access the gateway's `Otari-Request-ID`:

```python
result = client.with_response_metadata.message(
Expand Down
8 changes: 6 additions & 2 deletions src/otari/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
PROVIDER_NAME = "gateway"
GATEWAY_HEADER_NAME = "Otari-Key"

# Otari sets these on inference responses. Lookup is case-insensitive, so the casing is documentary.
REQUEST_ID_HEADER_NAME = "Otari-Request-ID"
ATTEMPT_ID_HEADER_NAME = "Otari-Attempt-ID"

# Locked phrasing used by the gateway to signal that the selected
# provider does not support a moderation request.
_UNSUPPORTED_MODERATION_RE = re.compile(r"does not support (?:multimodal )?moderation")
Expand Down Expand Up @@ -294,10 +298,10 @@ def map_api_exception(error: ApiException) -> OtariError:
status = error.status if isinstance(error.status, int) else 0
headers = error.headers or {}
detail = extract_detail(error)
correlation_id = _header_get(headers, "x-correlation-id")
attempt_id = _header_get(headers, ATTEMPT_ID_HEADER_NAME)
retry_after = _header_get(headers, "retry-after")

full = f"{detail} (correlation_id={correlation_id})" if correlation_id else detail
full = f"{detail} (attempt_id={attempt_id})" if attempt_id else detail

# Unsupported-capability is surfaced regardless of mode.
if status == 400 and _UNSUPPORTED_MODERATION_RE.search(detail):
Expand Down
6 changes: 3 additions & 3 deletions src/otari/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

import httpx

from otari._base import _BaseOtariClient, _header_get, build_request
from otari._base import REQUEST_ID_HEADER_NAME, _BaseOtariClient, _header_get, build_request
from otari._client import ApiClient, Configuration
from otari._client.api.batches_api import BatchesApi
from otari._client.api.chat_api import ChatApi
Expand Down Expand Up @@ -486,7 +486,7 @@ async def _call_with_response_metadata(
response = await self._call(fn)
return OtariResponse(
data=response.data,
request_id=_header_get(response.headers, "X-Otari-Request-ID"),
request_id=_header_get(response.headers, REQUEST_ID_HEADER_NAME),
)

async def _post(
Expand Down Expand Up @@ -544,7 +544,7 @@ async def _iter_stream(
raw = await response.aread()
raise self._map_streaming_response(response, raw)
if stream is not None:
stream._set_request_id(_header_get(response.headers, "X-Otari-Request-ID"))
stream._set_request_id(_header_get(response.headers, REQUEST_ID_HEADER_NAME))
async for chunk in aiter_sse(response, kind):
yield chunk

Expand Down
6 changes: 3 additions & 3 deletions src/otari/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

import httpx

from otari._base import _BaseOtariClient, _header_get, build_request
from otari._base import REQUEST_ID_HEADER_NAME, _BaseOtariClient, _header_get, build_request
from otari._client import ApiClient, Configuration
from otari._client.api.batches_api import BatchesApi
from otari._client.api.chat_api import ChatApi
Expand Down Expand Up @@ -507,7 +507,7 @@ def _call_with_response_metadata(self, fn: Callable[[], Any]) -> OtariResponse[A
response = self._call(fn)
return OtariResponse(
data=response.data,
request_id=_header_get(response.headers, "X-Otari-Request-ID"),
request_id=_header_get(response.headers, REQUEST_ID_HEADER_NAME),
)

def _post(
Expand Down Expand Up @@ -564,7 +564,7 @@ def _iter_stream(
raw = response.read()
raise self._map_streaming_response(response, raw)
if stream is not None:
stream._set_request_id(_header_get(response.headers, "X-Otari-Request-ID"))
stream._set_request_id(_header_get(response.headers, REQUEST_ID_HEADER_NAME))
yield from iter_sse(response, kind)

# -- Cleanup ------------------------------------------------------------
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async def test_response_with_metadata_exposes_request_id_without_changing_respon
mock_rest(
status=200,
body=RESPONSES_RESPONSE,
headers={"X-Otari-Request-ID": "req-async-response-123"},
headers={"Otari-Request-ID": "req-async-response-123"},
)
client = AsyncOtariClient(api_base="http://localhost:8000", api_key="vk")

Expand Down Expand Up @@ -115,7 +115,7 @@ async def test_message_with_response_metadata_exposes_request_id(self, mock_rest
mock_rest(
status=200,
body=MESSAGE_RESPONSE,
headers={"x-otari-request-id": "req-async-message-123"},
headers={"otari-request-id": "req-async-message-123"},
)
client = AsyncOtariClient(api_base="http://localhost:8000", api_key="vk")

Expand Down Expand Up @@ -204,7 +204,7 @@ async def test_response_stream_metadata_exposes_request_id_without_changing_even
200,
headers={
"content-type": "text/event-stream",
"X-Otari-Request-ID": "req-async-response-stream-123",
"Otari-Request-ID": "req-async-response-stream-123",
},
content=_sse(event),
)
Expand All @@ -230,7 +230,7 @@ async def test_message_stream_metadata_exposes_request_id_without_mutating_event
200,
headers={
"content-type": "text/event-stream",
"X-Otari-Request-ID": "req-async-stream-123",
"Otari-Request-ID": "req-async-stream-123",
},
content=_sse(event),
)
Expand Down
16 changes: 8 additions & 8 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def test_non_streaming_exposes_request_id_without_changing_response(self, mock_r
mock_rest(
status=200,
body=RESPONSES_RESPONSE,
headers={"X-Otari-Request-ID": "req-response-123"},
headers={"Otari-Request-ID": "req-response-123"},
)
client = OtariClient(api_base="http://localhost:8000", api_key="vk")

Expand All @@ -221,7 +221,7 @@ def test_streaming_exposes_request_id_without_changing_events(self) -> None:
200,
headers={
"content-type": "text/event-stream",
"X-Otari-Request-ID": "req-response-stream-123",
"Otari-Request-ID": "req-response-stream-123",
},
content=_sse(event),
)
Expand Down Expand Up @@ -277,7 +277,7 @@ def test_with_response_metadata_exposes_request_id(self, mock_rest: Any) -> None
mock_rest(
status=200,
body=MESSAGE_RESPONSE,
headers={"x-otari-request-id": "req-message-123"},
headers={"otari-request-id": "req-message-123"},
)
client = OtariClient(api_base="http://localhost:8000", api_key="vk")

Expand All @@ -295,7 +295,7 @@ def test_with_response_metadata_is_available_for_chat(self, mock_rest: Any) -> N
mock_rest(
status=200,
body=CHAT_RESPONSE,
headers={"X-Otari-Request-ID": "req-chat-123"},
headers={"Otari-Request-ID": "req-chat-123"},
)
client = OtariClient(api_base="http://localhost:8000", api_key="vk")

Expand Down Expand Up @@ -379,12 +379,12 @@ def test_rate_limit_carries_retry_after(self, mock_rest: Any) -> None:
client.completion(model="m", messages=[{"role": "user", "content": "Hi"}])
assert exc_info.value.retry_after == "30"

def test_correlation_id_in_message(self, mock_rest: Any) -> None:
mock_rest(status=402, body={"detail": "no funds"}, headers={"x-correlation-id": "abc-123"})
def test_attempt_id_in_message(self, mock_rest: Any) -> None:
mock_rest(status=402, body={"detail": "no funds"}, headers={"Otari-Attempt-ID": "abc-123"})
client = OtariClient(api_base="http://localhost:8000", api_key="vk")
with pytest.raises(InsufficientFundsError) as exc_info:
client.completion(model="m", messages=[{"role": "user", "content": "Hi"}])
assert "abc-123" in str(exc_info.value)
assert "attempt_id=abc-123" in str(exc_info.value)

def test_unsupported_moderation_maps_in_any_mode(self, mock_rest: Any) -> None:
mock_rest(
Expand Down Expand Up @@ -472,7 +472,7 @@ def test_message_stream_metadata_exposes_request_id_without_mutating_events(self
200,
headers={
"content-type": "text/event-stream",
"X-Otari-Request-ID": "req-stream-123",
"Otari-Request-ID": "req-stream-123",
},
content=_sse(event),
)
Expand Down
Loading