Skip to content

feat(provider): add new provider gateway traits#16

Merged
bzp2010 merged 5 commits intomainfrom
bzp/feat-new-gateway-traits
Apr 6, 2026
Merged

feat(provider): add new provider gateway traits#16
bzp2010 merged 5 commits intomainfrom
bzp/feat-new-gateway-traits

Conversation

@bzp2010
Copy link
Copy Markdown
Collaborator

@bzp2010 bzp2010 commented Apr 6, 2026

As the second part of the major refactoring of the provider, add traits for use by the new provider crate.

Summary by CodeRabbit

  • Documentation

    • Renamed internal doc to "LLM Type and Trait System" and added a "Trait stack" section detailing format/trait relationships and streaming semantics.
  • New Features

    • Added native streaming support and typed native-provider handlers for Anthropic/OpenAI-style APIs.
    • Introduced explicit stream state, tool-call assembly rules, and SSE/event integration hooks.
  • Refactor

    • Consolidated gateway trait surface and added declarative provider compatibility quirks.

Copilot AI review requested due to automatic review settings April 6, 2026 04:19
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e28e80a9-93d0-4bfe-81a8-3ef2dfc8a614

📥 Commits

Reviewing files that changed from the base of the PR and between 6bcd144 and 2172154.

📒 Files selected for processing (1)
  • src/gateway/traits/chat_format.rs

📝 Walkthrough

Walkthrough

Adds a trait-based gateway abstraction: ChatFormat for request/response/stream formatting, provider meta/transform/capabilities traits, native-format support with a type-erased NativeHandler, explicit stream state types and tool-call accumulation, compatibility quirks, module re-exports, and documentation updates. (50 words)

Changes

Cohort / File(s) Summary
Trait System Core
src/gateway/traits/chat_format.rs, src/gateway/traits/provider.rs
Adds ChatFormat trait (request/response/stream types, bridge/native stream state, native bypass hooks, SSE serializers), ChatStreamState, ToolCallAccumulator, and provider traits: ProviderMeta, ChatTransform, ProviderCapabilities, CompatQuirks, StreamReaderKind, plus marker transform traits (embed/tts/stt/image) and stream parsing logic.
Native Support & Handler
src/gateway/traits/native.rs
Adds native stream state structs and native support traits NativeAnthropicMessagesSupport and NativeOpenAIResponsesSupport; introduces type-erased NativeHandler<'a> and provider_name() dispatch.
Module Aggregation & Exports
src/gateway/traits/mod.rs, src/gateway/mod.rs
New traits module declaring submodules and re-exporting key types; src/gateway/mod.rs now publicly exposes traits.
Documentation
docs/internals/llm-types.md
Renamed title and added “Trait stack” section documenting ChatFormat axes, provider transform/capabilities axis, explicit stream state types, hub stream tool-call assembly, native support traits, and extended CompatQuirks behaviors.
Minor doc ref updates
src/gateway/types/anthropic.rs, src/gateway/types/openai/responses.rs
Updated module comments to reference renamed native support traits (NativeAnthropicMessagesSupport, NativeOpenAIResponsesSupport).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Gateway
  participant Provider
  participant Hub as "Hub/Backend"

  Note over Gateway,Provider: ChatFormat + ProviderCapabilities used for transforms

  Client->>Gateway: Submit ChatFormat::Request
  Gateway->>Gateway: ChatFormat::is_stream / extract_model
  Gateway->>Provider: build_url / transform_request
  alt Provider exposes native support
    Gateway->>Provider: native_support() -> NativeHandler
    Gateway->>Provider: call_native (via NativeHandler)
    Provider-->>Gateway: native stream chunks / final response
    Gateway->>Gateway: transform_native_stream_chunk -> ChatFormat::StreamChunk
  else Use hub (OpenAI-compatible)
    Gateway->>Hub: POST transformed request
    Hub-->>Gateway: hub streaming chunks / final response
    Gateway->>Gateway: from_hub_stream / from_hub -> ChatFormat::StreamChunk/Response
  end
  Gateway-->>Client: Emit StreamChunk/Response (serialize_chunk_payload / SSE)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • LiteSun
🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
E2e Test Quality Review ⚠️ Warning native.rs module is completely untested; tests only verify default implementations without validating trait contracts or implementability. Add comprehensive unit tests for NativeAnthropicMessagesSupport and NativeOpenAIResponsesSupport traits; create concrete provider implementations validating the three-layer trait design.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(provider): add new provider gateway traits' directly and accurately describes the main change: introducing new trait abstractions for the provider gateway system.

✏️ 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 bzp/feat-new-gateway-traits

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new gateway-side trait stack to support the refactored provider crate, including capability discovery and optional “native bypass” paths for Anthropic Messages and OpenAI Responses.

Changes:

  • Introduces gateway::traits with ChatFormat, provider metadata/transform traits, and native support traits.
  • Adds CompatQuirks to centralize small OpenAI-compatibility adjustments (param removal/rename, stream usage injection, done sentinel).
  • Updates internal docs and type-module doc comments to reflect the renamed native support traits.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/gateway/types/openai/responses.rs Doc comment update to reference NativeOpenAIResponsesSupport.
src/gateway/types/anthropic.rs Doc comment update to reference NativeAnthropicMessagesSupport.
src/gateway/traits/provider.rs New provider trait layer (ProviderMeta, ChatTransform, ProviderCapabilities) and CompatQuirks.
src/gateway/traits/native.rs New native bypass traits and native stream state types.
src/gateway/traits/mod.rs Exposes the new trait modules and re-exports key types.
src/gateway/traits/chat_format.rs Defines the ChatFormat contract and shared stream state helpers.
src/gateway/mod.rs Exposes the new gateway::traits module.
docs/internals/llm-types.md Documents the combined type + trait architecture and native bypass design.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/gateway/traits/provider.rs (1)

41-46: Normalize URL joining to avoid malformed endpoints.

build_url depends on chat_endpoint_path() returning a leading slash. Normalizing both sides makes this robust across provider implementations.

Suggested refactor
     fn build_url(&self, base_url: &str, model: &str) -> String {
-        format!(
-            "{}{}",
-            base_url.trim_end_matches('/'),
-            self.chat_endpoint_path(model)
-        )
+        let endpoint = self.chat_endpoint_path(model);
+        let endpoint = endpoint.as_ref().trim_start_matches('/');
+        format!("{}/{}", base_url.trim_end_matches('/'), endpoint)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/traits/provider.rs` around lines 41 - 46, build_url currently
concatenates base_url and chat_endpoint_path assuming the latter starts with
'/', which can produce malformed endpoints; change build_url to normalize both
parts by trimming trailing slashes from base_url and trimming leading slashes
from the result of chat_endpoint_path(model), then join with a single '/' (and
if chat_endpoint_path returns empty, return the trimmed base_url). Update the
build_url implementation to use base_url.trim_end_matches('/') and
chat_endpoint_path(model).trim_start_matches('/') and ensure you handle an empty
path case so provider implementations no longer need to supply a leading slash.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/gateway/traits/chat_format.rs`:
- Around line 88-95: The default implementation of parse_native_response
currently panics via unreachable! which can crash request handling; change it to
return a proper Err value instead: return an appropriate typed error using the
crate's Result/Error type (the same error type used elsewhere in this module)
with a clear message like "parse_native_response called on a non-native format",
so callers get a recoverable failure; update the parse_native_response method
signature implementation (the function named parse_native_response that takes
&NativeHandler<'_> and body: Value and returns Result<Self::Response>) to
construct and return that error rather than invoking unreachable!.

In `@src/gateway/traits/provider.rs`:
- Around line 67-80: transform_stream_chunk currently tries to JSON-parse any
non-empty input which causes errors for SSE control lines and bare done signals;
update transform_stream_chunk (and use default_quirks()/stream_done_signal) to
first handle SSE control frames safely: if the trimmed raw equals the
stream_done_signal (e.g., "[DONE]") return Ok(vec![]); if the raw is an SSE
comment (starts with ":"), an event line (starts with "event:"), or does not
contain a "data:" payload then return Ok(vec![]) without parsing; only call
serde_json::from_str on the actual data payload extracted via
strip_prefix("data: "). Ensure error mapping remains GatewayError::Transform for
parse failures.

---

Nitpick comments:
In `@src/gateway/traits/provider.rs`:
- Around line 41-46: build_url currently concatenates base_url and
chat_endpoint_path assuming the latter starts with '/', which can produce
malformed endpoints; change build_url to normalize both parts by trimming
trailing slashes from base_url and trimming leading slashes from the result of
chat_endpoint_path(model), then join with a single '/' (and if
chat_endpoint_path returns empty, return the trimmed base_url). Update the
build_url implementation to use base_url.trim_end_matches('/') and
chat_endpoint_path(model).trim_start_matches('/') and ensure you handle an empty
path case so provider implementations no longer need to supply a leading slash.
🪄 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: CHILL

Plan: Pro

Run ID: 07e0c84a-191e-4a6c-95bf-95d3ee6c583d

📥 Commits

Reviewing files that changed from the base of the PR and between 7c9ab68 and 7747d24.

📒 Files selected for processing (8)
  • docs/internals/llm-types.md
  • src/gateway/mod.rs
  • src/gateway/traits/chat_format.rs
  • src/gateway/traits/mod.rs
  • src/gateway/traits/native.rs
  • src/gateway/traits/provider.rs
  • src/gateway/types/anthropic.rs
  • src/gateway/types/openai/responses.rs

@bzp2010 bzp2010 requested a review from Copilot April 6, 2026 04:52
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/gateway/traits/provider.rs (1)

28-31: Normalize the join between base_url and chat_endpoint_path().

The default path includes a leading slash, but this trait contract does not require one. An implementation returning v1/chat/completions would currently produce a broken URL.

Suggested fix
     fn build_url(&self, base_url: &str, model: &str) -> String {
-        format!(
-            "{}{}",
-            base_url.trim_end_matches('/'),
-            self.chat_endpoint_path(model)
-        )
+        let path = self.chat_endpoint_path(model);
+        format!(
+            "{}/{}",
+            base_url.trim_end_matches('/'),
+            path.trim_start_matches('/')
+        )
     }

Also applies to: 41-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/traits/provider.rs` around lines 28 - 31, The URL join currently
assumes the endpoint path starts with a slash which breaks when implementations
return e.g. "v1/chat/completions"; update the join logic to normalize both sides
by trimming any trailing '/' from the provider's base URL and any leading '/'
from chat_endpoint_path (and other endpoint path methods) and then concatenate
them with a single '/' between; locate usages of chat_endpoint_path (and the
other endpoint path methods around lines 41-46) and replace the direct
concatenation with this normalization to ensure a correct URL regardless of
whether implementations include a leading slash.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/gateway/traits/chat_format.rs`:
- Around line 120-123: The tool_call_accumulators map in ChatStreamState
currently keys by a single usize which collides across different
ChunkToolCall::index values from different choices; change
ChatStreamState.tool_call_accumulators from HashMap<usize, ToolCallAccumulator>
to HashMap<(usize, usize), ToolCallAccumulator> (key = (choice_index,
tool_call_index)) and update every read/write site that indexes or inserts into
this map (where ChunkToolCall::index and the enclosing choice index are used —
e.g., during stream chunk handling and aggregation) to construct and use the
(choice_index, tool_call_index) tuple key so fragments from different choices do
not merge.

In `@src/gateway/traits/provider.rs`:
- Around line 16-21: The ProviderAuth enum currently derives Debug which will
expose the raw ApiKey(String); replace the auto-derived Debug with a manual
implementation that redacts secrets: remove #[derive(Debug)] from ProviderAuth
and implement std::fmt::Debug for ProviderAuth so the ApiKey variant prints
something like "ApiKey(REDACTED)" while the None variant prints "None"; keep or
re-add #[derive(Clone, Default)] so Clone and Default behavior remain unchanged
and ensure any logging uses the Debug impl to avoid leaking the actual key.
- Around line 170-173: The loop handling self.param_renames currently removes
the source and unconditionally inserts into the destination, which overwrites
any explicitly provided destination; change the logic in that loop (the block
using self.param_renames, map.remove and map.insert) to first check whether the
destination key (*to) already exists in map and only perform the rename insert
when the destination is not present (i.e., preserve caller-provided destination
values), otherwise leave the destination value untouched and drop or keep the
source as appropriate.

---

Nitpick comments:
In `@src/gateway/traits/provider.rs`:
- Around line 28-31: The URL join currently assumes the endpoint path starts
with a slash which breaks when implementations return e.g.
"v1/chat/completions"; update the join logic to normalize both sides by trimming
any trailing '/' from the provider's base URL and any leading '/' from
chat_endpoint_path (and other endpoint path methods) and then concatenate them
with a single '/' between; locate usages of chat_endpoint_path (and the other
endpoint path methods around lines 41-46) and replace the direct concatenation
with this normalization to ensure a correct URL regardless of whether
implementations include a leading slash.
🪄 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: CHILL

Plan: Pro

Run ID: 06cd525f-292e-47d9-9157-bf802686e815

📥 Commits

Reviewing files that changed from the base of the PR and between 7747d24 and c946944.

📒 Files selected for processing (3)
  • src/gateway/traits/chat_format.rs
  • src/gateway/traits/native.rs
  • src/gateway/traits/provider.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/gateway/traits/native.rs

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.


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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/gateway/traits/provider.rs (1)

76-102: Fail fast for non-SSE defaults to avoid silent empty streams.

At Line 100–Line 102, any non-data: chunk is silently ignored. That’s fine for SSE, but for providers using StreamReaderKind::AwsEventStream or StreamReaderKind::JsonArrayStream, a missing override can quietly drop all chunks. Consider guarding on stream_reader_kind() and returning a transform error for non-SSE defaults.

♻️ Suggested change
     fn transform_stream_chunk(
         &self,
         raw: &str,
         _state: &mut ChatStreamState,
     ) -> Result<Vec<ChatCompletionChunk>> {
+        if self.stream_reader_kind() != StreamReaderKind::Sse {
+            return Err(GatewayError::Transform(
+                "default transform_stream_chunk only supports SSE providers; override for non-SSE stream readers".to_string(),
+            ));
+        }
+
         let quirks = self.default_quirks();
         let trimmed = raw.trim();
         let done_signal = quirks.stream_done_signal.trim();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/traits/provider.rs` around lines 76 - 102, In
transform_stream_chunk, currently any non-"data:" chunk is silently ignored
which breaks providers whose default stream_reader_kind() is not SSE; update the
logic in transform_stream_chunk (and use self.default_quirks() and
self.stream_reader_kind()) to detect when stream_reader_kind() !=
StreamReaderKind::Sse (e.g., AwsEventStream or JsonArrayStream) and, instead of
returning Ok(vec![]) for a non-"data:" line, return an Err(...) using the same
Result error type (a transform error) indicating unexpected chunk format so
missing overrides fail fast; leave SSE behavior (skipping non-data lines)
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/gateway/traits/provider.rs`:
- Around line 76-102: In transform_stream_chunk, currently any non-"data:" chunk
is silently ignored which breaks providers whose default stream_reader_kind() is
not SSE; update the logic in transform_stream_chunk (and use
self.default_quirks() and self.stream_reader_kind()) to detect when
stream_reader_kind() != StreamReaderKind::Sse (e.g., AwsEventStream or
JsonArrayStream) and, instead of returning Ok(vec![]) for a non-"data:" line,
return an Err(...) using the same Result error type (a transform error)
indicating unexpected chunk format so missing overrides fail fast; leave SSE
behavior (skipping non-data lines) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a52e3d3-b933-4a45-826a-84ce6a9d036a

📥 Commits

Reviewing files that changed from the base of the PR and between c946944 and 7e088bc.

📒 Files selected for processing (1)
  • src/gateway/traits/provider.rs

Copilot AI review requested due to automatic review settings April 6, 2026 06:53
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.


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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/gateway/traits/chat_format.rs (1)

82-86: Consider adding a default implementation for transform_native_stream_chunk.

Unlike call_native and parse_native_response which provide defaults returning appropriate errors, this method is required. Formats that don't support native streaming must still implement a stub.

Additionally, this method takes &dyn ProviderCapabilities while the other native methods take &NativeHandler<'_>. If this signature difference is intentional (e.g., the caller needs to query capabilities before knowing which native path to use), consider adding a brief doc comment explaining why.

♻️ Suggested default implementation for consistency
     /// Convert a native streaming chunk into zero or more chunks of this format.
     fn transform_native_stream_chunk(
         provider: &dyn ProviderCapabilities,
         raw: &str,
         state: &mut Self::NativeStreamState,
-    ) -> Result<Vec<Self::StreamChunk>>;
+    ) -> Result<Vec<Self::StreamChunk>>
+    where
+        Self: Sized,
+    {
+        let _ = (provider, raw, state);
+        Err(GatewayError::NativeNotSupported {
+            provider: provider.name().into(),
+        })
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/traits/chat_format.rs` around lines 82 - 86, The trait forces
implementors to provide transform_native_stream_chunk while other native methods
(call_native, parse_native_response) have defaults; add a default implementation
for transform_native_stream_chunk on the trait that returns an Err (or other
appropriate "not supported" error) so formats that don't support native
streaming can omit a stub, and add a brief doc comment on
transform_native_stream_chunk explaining why it accepts &dyn
ProviderCapabilities (versus &NativeHandler<'_> used by other native methods) if
the caller must inspect capabilities before selecting the native path; reference
the transform_native_stream_chunk method and make its default behavior
consistent with call_native/parse_native_response.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/gateway/traits/chat_format.rs`:
- Around line 82-86: The trait forces implementors to provide
transform_native_stream_chunk while other native methods (call_native,
parse_native_response) have defaults; add a default implementation for
transform_native_stream_chunk on the trait that returns an Err (or other
appropriate "not supported" error) so formats that don't support native
streaming can omit a stub, and add a brief doc comment on
transform_native_stream_chunk explaining why it accepts &dyn
ProviderCapabilities (versus &NativeHandler<'_> used by other native methods) if
the caller must inspect capabilities before selecting the native path; reference
the transform_native_stream_chunk method and make its default behavior
consistent with call_native/parse_native_response.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bc564241-8522-4036-867f-a3a493d92bcb

📥 Commits

Reviewing files that changed from the base of the PR and between 7e088bc and 6bcd144.

📒 Files selected for processing (2)
  • docs/internals/llm-types.md
  • src/gateway/traits/chat_format.rs
✅ Files skipped from review due to trivial changes (1)
  • docs/internals/llm-types.md

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.

2 participants