Skip to content

fix(otari): preserve cache_control via native /messages pass-through - #1121

Merged
njbrake merged 2 commits into
mainfrom
fix/otari-amessages-native-passthrough
Jun 11, 2026
Merged

njbrake merged 2 commits into
mainfrom
fix/otari-amessages-native-passthrough

Conversation

@njbrake

@njbrake njbrake commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Description

OtariProvider inherited the base _amessages(), which converts Anthropic Messages to Chat Completions and silently drops Anthropic-only features (cache_control on system blocks, thinking config). For applications relying on Anthropic prompt caching, this meant every request through otari lost caching, raising latency and cost.

otari's gateway serves /messages natively, and otari SDK 0.1.0 now exposes AsyncOtariClient.message(). This PR overrides _amessages() to delegate to otari_client.message(), mirroring how _acompletion() delegates to otari_client.completion(). The transport (auth, URL, SSE parsing) stays inside the otari SDK where it belongs.

  • Non-streaming results validate into MessageResponse.
  • Streaming raw event dicts from otari's /messages SSE stream are mapped to typed MessageStreamEvent models; unknown event types (e.g. ping) are skipped.
  • The deprecated GatewayProvider subclass inherits the fix via the shared client.

This supersedes the raw-httpx approach in #1111 (closed) by keeping transport in the SDK rather than reimplementing it in any-llm.

PR Type

  • 🐛 Bug Fix

Relevant issues

Fixes #1110

Checklist

  • I understand the code I am submitting.
  • I have added unit tests that prove my fix/feature works
  • I have run this code locally and verified it fixes the issue.
  • New and existing tests pass locally
  • Documentation was updated where necessary
  • I have read and followed the contribution guidelines
  • AI Usage:
    • This is fully AI-generated.

AI Usage Information

  • AI Model used: Claude Opus 4.8

  • AI Developer Tool used: Claude Code

  • Any other info you'd like to share: Authored by Claude via back-and-forth with @njbrake. The reasoning and decisions are his.

  • I am an AI Agent filling out this form (check box if true)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Native support for Anthropic-style messaging, including streaming responses and typed event delivery.
    • Anthropic-specific settings (e.g. system controls and thinking behaviour) are preserved for native message operations.
  • Tests

    • New unit tests covering native messaging, streaming event ordering and filtering, and preservation of request fields.

OtariProvider inherited the base _amessages(), which converts Anthropic
Messages to Chat Completions and silently drops Anthropic-only features
(cache_control on system blocks, thinking config). otari's gateway serves
/messages natively, and otari SDK 0.1.0 now exposes AsyncOtariClient.message().

Override _amessages() to delegate to otari_client.message(), mirroring how
_acompletion() delegates to otari_client.completion(). Non-streaming results
validate into MessageResponse; streaming raw event dicts are mapped to typed
MessageStreamEvent models (unknown types like ping are skipped). The deprecated
GatewayProvider subclass inherits the fix via the shared client.

This supersedes the raw-httpx approach in #1111 by keeping transport in the SDK.

Fixes #1110

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests June 11, 2026 15:01 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: af136cbb-6985-45d3-bf2e-ec3c7a90e20d

📥 Commits

Reviewing files that changed from the base of the PR and between a230437 and 1a3f3f9.

📒 Files selected for processing (1)
  • src/any_llm/providers/otari/otari.py

Walkthrough

OtariProvider now overrides _amessages() to send Anthropic-format requests directly to otari's /messages endpoint. Non-streaming calls use otari_client.message and validate into MessageResponse; streaming calls consume otari SSE, convert raw event dicts to typed MessageStreamEvent instances, and yield only known event types.

Changes

Otari native messages endpoint

Layer / File(s) Summary
Event stream typing and mapping foundation
src/any_llm/providers/otari/otari.py, tests/unit/providers/test_otari_provider.py
Message event models are imported and a mapping from otari SSE type strings to typed MessageStreamEvent classes is added, plus _message_stream_event_from_dict which validates raw event dicts or returns None for unknown types. Unit tests verify known/unknown type handling.
Messages endpoint delegation and response streaming
src/any_llm/providers/otari/otari.py, tests/unit/providers/test_otari_provider.py
OtariProvider._amessages delegates non-streaming requests to otari_client.message and parses into MessageResponse, preserving Anthropic fields and omitting unset optional params. Streaming requests call _stream_messages_async, convert SSE event dicts into typed MessageStreamEvent items, and yield only supported events; tests cover delegation, parameter omission, and streaming behaviour.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly references the primary change: adding native /messages pass-through to preserve cache_control in OtariProvider, which directly addresses issue #1110.
Description check ✅ Passed The description is comprehensive, covering what the PR does, the PR type (bug fix), linked issues (#1110), completed checklist items, and detailed AI usage information.
Linked Issues check ✅ Passed The PR implementation fully addresses issue #1110's objectives: native /messages delegation via otari SDK preserves cache_control, thinking config, and system prompt structure while avoiding conversion to OpenAI Chat Completions.
Out of Scope Changes check ✅ Passed All changes in the PR are directly scoped to fixing the cache_control preservation issue: OtariProvider._amessages() implementation, streaming event mapping, and comprehensive unit tests covering the new functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/otari-amessages-native-passthrough

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/any_llm/providers/otari/otari.py 91.75% <100.00%> (-1.22%) ⬇️

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@njbrake
njbrake requested a review from khaledosman June 11, 2026 15:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/any_llm/providers/otari/otari.py`:
- Around line 286-292: In _stream_messages_async, make the stream resilient by
tracking whether a MessageStreamEvent with type "message_stop" (or equivalent
finish event) was yielded: set a local flag (e.g., message_stop_seen) before the
async for loop, update it when converted.type indicates stop, and after the loop
if the flag is false emit/yield a synthetic message_stop MessageStreamEvent
constructed via the same helper used for conversion
(_message_stream_event_from_dict or a minimal MessageStreamEvent instance) so
callers always receive a termination event even if the otari client or network
truncates the stream.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0446f26-68f8-4840-baf4-c3d7d12e03e3

📥 Commits

Reviewing files that changed from the base of the PR and between 3f9aaaa and a230437.

📒 Files selected for processing (2)
  • src/any_llm/providers/otari/otari.py
  • tests/unit/providers/test_otari_provider.py

Comment on lines +286 to +292
async def _stream_messages_async(self, **api_kwargs: Any) -> AsyncIterator[MessageStreamEvent]:
"""Stream otari's /messages endpoint, yielding typed any-llm event models."""
stream = await self.otari_client.message(stream=True, **api_kwargs)
async for event in stream:
converted = _message_stream_event_from_dict(_as_plain_dict(event))
if converted is not None:
yield converted

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider defensive stream termination for resilience.

The implementation trusts the otari SDK to always send a message_stop event. The base provider implementation (in any_llm.py) includes fallback logic to ensure message_stop is emitted even when the upstream provider doesn't send a final chunk with finish_reason.

Whilst this follows the existing pattern in _acompletion (lines 245–263) and tests confirm otari sends the termination event, adding a fallback (e.g., tracking whether message_stop was seen and emitting it if missing) would improve resilience against future changes in otari SDK behaviour or network truncation.

🧰 Tools
🪛 Ruff (0.15.15)

[warning] 286-286: Dynamically typed expressions (typing.Any) are disallowed in **api_kwargs

(ANN401)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/any_llm/providers/otari/otari.py` around lines 286 - 292, In
_stream_messages_async, make the stream resilient by tracking whether a
MessageStreamEvent with type "message_stop" (or equivalent finish event) was
yielded: set a local flag (e.g., message_stop_seen) before the async for loop,
update it when converted.type indicates stop, and after the loop if the flag is
false emit/yield a synthetic message_stop MessageStreamEvent constructed via the
same helper used for conversion (_message_stream_event_from_dict or a minimal
MessageStreamEvent instance) so callers always receive a termination event even
if the otari client or network truncates the stream.

khaledosman
khaledosman previously approved these changes Jun 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes Otari’s amessages() behavior by bypassing the default Messages→Chat Completions conversion and delegating directly to otari’s native /messages support via the otari SDK, preserving Anthropic-only fields like cache_control and thinking.

Changes:

  • Override OtariProvider._amessages() to call otari_client.message() for native Messages API support (streaming + non-streaming).
  • Add SSE event mapping from raw otari /messages stream events into typed MessageStreamEvent models (skipping unknown event types).
  • Add unit tests validating pass-through of Anthropic-specific fields, omission of None optionals, and typed streaming behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/any_llm/providers/otari/otari.py Overrides _amessages() to use otari SDK native /messages, plus stream event dict→model conversion.
tests/unit/providers/test_otari_provider.py Adds tests for native _amessages() delegation, streaming event typing, and parameter filtering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/any_llm/providers/otari/otari.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@njbrake
njbrake temporarily deployed to integration-tests June 11, 2026 15:31 — with GitHub Actions Inactive
@njbrake
njbrake merged commit e99f7a1 into main Jun 11, 2026
10 of 11 checks passed
@njbrake
njbrake deleted the fix/otari-amessages-native-passthrough branch June 11, 2026 15:32

This branch was previously deployed

1 inactive deployment
integration-tests — 1a3f3f98 Deployed Jun 11, 2026 by njbrake via run-docs-tests #2040
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OtariProvider drops cache control during Anthropic-to-OpenAI message conversion

3 participants