xz_provider/protocol/mod.rs
1pub mod openai_chat;
2pub use openai_chat::OpenAiChatAdapter;
3
4/// Shared OpenAI wire encode/decode helpers (chat + responses).
5pub(crate) mod openai_wire;
6
7use std::fmt::Debug;
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12pub mod openai_responses;
13
14use crate::error::ProviderError;
15use crate::types::{CompletionRequest, CompletionResponse, StreamEvent};
16
17#[cfg(feature = "anthropic")]
18pub mod anthropic;
19
20/// Authentication method for a provider.
21///
22/// Represents the various ways providers authenticate API requests.
23/// The [`ProtocolAdapter::build_auth_headers`] method converts this
24/// into the appropriate HTTP headers.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(tag = "type", rename_all = "snake_case")]
27pub enum AuthMethod {
28 /// No authentication required (e.g., local providers).
29 None,
30 /// Bearer token authentication (used by OpenAI, Anthropic, etc.).
31 Bearer {
32 /// The bearer token value (api key).
33 token: String,
34 },
35 /// Custom API key header authentication.
36 ApiKey {
37 /// The HTTP header name for the API key (e.g., "x-api-key").
38 header_name: String,
39 /// The API key value.
40 key: String,
41 },
42}
43
44/// A protocol adapter translates between the unified request/response types
45/// and a specific provider's API protocol.
46///
47/// This trait is object-safe so it can be used as `&dyn ProtocolAdapter`
48/// or `Box<dyn ProtocolAdapter>`. That means:
49/// - No async methods
50/// - No generic type parameters
51/// - No `impl Trait` return types
52///
53/// Each variant represents a different API protocol:
54/// - OpenAI Chat Completions (`/v1/chat/completions`)
55/// - OpenAI Responses (`/v1/responses`)
56/// - Anthropic Messages (`/v1/messages`)
57/// - Ollama (`/api/chat`)
58///
59/// # Examples
60///
61/// ```rust
62/// use xz_provider::protocol::{ProtocolAdapter, AuthMethod};
63/// use xz_provider::ProviderError;
64///
65/// # #[derive(Debug)]
66/// # struct DummyAdapter;
67/// # impl ProtocolAdapter for DummyAdapter {
68/// # fn endpoint_path(&self) -> &str { "/v1/test" }
69/// # fn build_request_body(&self, _: &xz_provider::CompletionRequest, _: bool) -> Result<serde_json::Value, ProviderError> {
70/// # Ok(serde_json::json!({}))
71/// # }
72/// # fn build_auth_headers(&self, _: &AuthMethod) -> Vec<(String, String)> { vec![] }
73/// # fn parse_response(&self, _: &serde_json::Value) -> Result<xz_provider::CompletionResponse, ProviderError> {
74/// # Err(ProviderError::Format("not implemented".to_owned()))
75/// # }
76/// # fn parse_sse_event(&self, _: &str) -> Result<Option<xz_provider::StreamEvent>, ProviderError> {
77/// # Ok(None)
78/// # }
79/// # fn protocol_name(&self) -> &str { "test" }
80/// # }
81/// let adapter: &dyn ProtocolAdapter = &DummyAdapter;
82/// assert_eq!(adapter.protocol_name(), "test");
83/// ```
84pub trait ProtocolAdapter: Debug + Send + Sync {
85 /// Returns the API endpoint path for this protocol (e.g., `/v1/chat/completions`).
86 ///
87 /// The caller appends this to the provider's base URL.
88 fn endpoint_path(&self) -> &str;
89
90 /// Builds the JSON request body for a completion request.
91 ///
92 /// When `stream` is `true`, the body should include the
93 /// protocol-appropriate streaming flag (e.g., `"stream": true` for
94 /// OpenAI, no change for Anthropic which uses SSE headers instead).
95 fn build_request_body(
96 &self,
97 request: &CompletionRequest,
98 stream: bool,
99 ) -> Result<Value, ProviderError>;
100
101 /// Converts an [`AuthMethod`] into the appropriate HTTP auth headers.
102 ///
103 /// For `AuthMethod::None`, returns an empty vector.
104 /// For `AuthMethod::Bearer`, returns a single `Authorization: Bearer ...` header.
105 /// For `AuthMethod::ApiKey`, returns a single header with the specified name and value.
106 fn build_auth_headers(&self, auth: &AuthMethod) -> Vec<(String, String)>;
107
108 /// Parses a complete (non-streaming) JSON response body into a
109 /// [`CompletionResponse`].
110 fn parse_response(&self, body: &Value) -> Result<CompletionResponse, ProviderError>;
111
112 /// Parses the data portion of a single SSE event into a [`StreamEvent`].
113 ///
114 /// Returns `Ok(None)` when the event is ignorable (e.g., a heartbeat).
115 /// The input `data` string is the content after the `data: ` prefix,
116 /// already trimmed.
117 fn parse_sse_event(&self, data: &str) -> Result<Option<StreamEvent>, ProviderError>;
118
119 /// Returns the human-readable name of this protocol for logging and metrics.
120 fn protocol_name(&self) -> &str;
121}