moved away from string literals to consts

This commit is contained in:
Salman Paracha 2025-09-06 10:47:05 -07:00
parent 2de75d18db
commit 06c71c1392
7 changed files with 36 additions and 155 deletions

View file

@ -4,7 +4,7 @@ use brightstaff::router::llm_router::RouterService;
use brightstaff::utils::tracing::init_tracer;
use bytes::Bytes;
use common::configuration::Configuration;
use common::consts::CHAT_COMPLETIONS_PATH;
use common::consts::{CHAT_COMPLETIONS_PATH, MESSAGES_PATH};
use http_body_util::{combinators::BoxBody, BodyExt, Empty};
use hyper::body::Incoming;
use hyper::server::conn::http1;
@ -112,7 +112,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
async move {
match (req.method(), req.uri().path()) {
(&Method::POST, "/v1/chat/completions" | "/v1/messages") => {
(&Method::POST, CHAT_COMPLETIONS_PATH | MESSAGES_PATH) => {
let fully_qualified_url = format!("{}{}", llm_provider_url, req.uri().path());
chat(req, router_service, fully_qualified_url)
.with_context(parent_cx)

View file

@ -8,6 +8,7 @@ use super::ApiDefinition;
use crate::providers::request::{ProviderRequest, ProviderRequestError};
use crate::providers::response::ProviderStreamResponse;
use crate::clients::transformer::ExtractText;
use crate::{MESSAGES_PATH};
// Enum for all supported Anthropic APIs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -21,13 +22,13 @@ pub enum AnthropicApi {
impl ApiDefinition for AnthropicApi {
fn endpoint(&self) -> &'static str {
match self {
AnthropicApi::Messages => "/v1/messages",
AnthropicApi::Messages => MESSAGES_PATH,
}
}
fn from_endpoint(endpoint: &str) -> Option<Self> {
match endpoint {
"/v1/messages" => Some(AnthropicApi::Messages),
MESSAGES_PATH => Some(AnthropicApi::Messages),
_ => None,
}
}
@ -1044,13 +1045,13 @@ mod tests {
let api = AnthropicApi::Messages;
// Test trait methods
assert_eq!(api.endpoint(), "/v1/messages");
assert_eq!(api.endpoint(), MESSAGES_PATH);
assert!(api.supports_streaming());
assert!(api.supports_tools());
assert!(api.supports_vision());
// Test from_endpoint trait method
let found_api = AnthropicApi::from_endpoint("/v1/messages");
let found_api = AnthropicApi::from_endpoint(MESSAGES_PATH);
assert_eq!(found_api, Some(AnthropicApi::Messages));
let not_found = AnthropicApi::from_endpoint("/v1/unknown");

View file

@ -1,110 +1,9 @@
pub mod anthropic;
pub mod openai;
// Re-export all types for convenience
pub use anthropic::*;
pub use openai::*;
/// Common trait that all API definitions must implement
///
/// This trait ensures consistency across different AI provider API definitions
/// and makes it easy to add new providers like Gemini, Claude, etc.
///
/// Note: This is different from the `ApiProvider` enum in `clients::endpoints`
/// which represents provider identification, while this trait defines API capabilities.
///
/// # Benefits
///
/// - **Consistency**: All API providers implement the same interface
/// - **Extensibility**: Easy to add new providers without breaking existing code
/// - **Type Safety**: Compile-time guarantees that all providers implement required methods
/// - **Discoverability**: Clear documentation of what capabilities each API supports
///
/// # Example implementation for a new provider:
///
/// ```rust,ignore
/// use serde::{Deserialize, Serialize};
/// use super::ApiDefinition;
///
/// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// pub enum GeminiApi {
/// GenerateContent,
/// ChatCompletions,
/// }
///
/// impl GeminiApi {
/// pub fn endpoint(&self) -> &'static str {
/// match self {
/// GeminiApi::GenerateContent => "/v1/models/gemini-pro:generateContent",
/// GeminiApi::ChatCompletions => "/v1/models/gemini-pro:chat",
/// }
/// }
///
/// pub fn from_endpoint(endpoint: &str) -> Option<Self> {
/// match endpoint {
/// "/v1/models/gemini-pro:generateContent" => Some(GeminiApi::GenerateContent),
/// "/v1/models/gemini-pro:chat" => Some(GeminiApi::ChatCompletions),
/// _ => None,
/// }
/// }
///
/// pub fn supports_streaming(&self) -> bool {
/// match self {
/// GeminiApi::GenerateContent => true,
/// GeminiApi::ChatCompletions => true,
/// }
/// }
///
/// pub fn supports_tools(&self) -> bool {
/// match self {
/// GeminiApi::GenerateContent => true,
/// GeminiApi::ChatCompletions => false,
/// }
/// }
///
/// pub fn supports_vision(&self) -> bool {
/// match self {
/// GeminiApi::GenerateContent => true,
/// GeminiApi::ChatCompletions => false,
/// }
/// }
/// }
///
/// impl ApiDefinition for GeminiApi {
/// fn endpoint(&self) -> &'static str {
/// self.endpoint()
/// }
///
/// fn from_endpoint(endpoint: &str) -> Option<Self> {
/// Self::from_endpoint(endpoint)
/// }
///
/// fn supports_streaming(&self) -> bool {
/// self.supports_streaming()
/// }
///
/// fn supports_tools(&self) -> bool {
/// self.supports_tools()
/// }
///
/// fn supports_vision(&self) -> bool {
/// self.supports_vision()
/// }
/// }
///
/// // Now you can use generic code that works with any API:
/// fn print_api_info<T: ApiDefinition>(api: &T) {
/// println!("Endpoint: {}", api.endpoint());
/// println!("Supports streaming: {}", api.supports_streaming());
/// println!("Supports tools: {}", api.supports_tools());
/// println!("Supports vision: {}", api.supports_vision());
/// }
///
/// // Works with both OpenAI and Anthropic (and future Gemini)
/// print_api_info(&OpenAIApi::ChatCompletions);
/// print_api_info(&AnthropicApi::Messages);
/// print_api_info(&GeminiApi::GenerateContent);
/// ```
pub trait ApiDefinition {
/// Returns the endpoint path for this API
fn endpoint(&self) -> &'static str;
@ -132,6 +31,7 @@ pub trait ApiDefinition {
#[cfg(test)]
mod tests {
use super::*;
use crate::{CHAT_COMPLETIONS_PATH, MESSAGES_PATH};
#[test]
fn test_generic_api_functionality() {
@ -150,8 +50,8 @@ mod tests {
fn test_api_detection_from_endpoints() {
// Test that we can detect APIs from endpoints using the trait
let endpoints = vec![
"/v1/chat/completions",
"/v1/messages",
CHAT_COMPLETIONS_PATH,
MESSAGES_PATH,
"/v1/unknown"
];

View file

@ -9,6 +9,7 @@ use crate::providers::request::{ProviderRequest, ProviderRequestError};
use crate::providers::response::{ProviderResponse, ProviderStreamResponse, TokenUsage};
use super::ApiDefinition;
use crate::clients::transformer::{ExtractText};
use crate::{CHAT_COMPLETIONS_PATH};
// ============================================================================
// OPENAI API ENUMERATION
@ -27,13 +28,13 @@ pub enum OpenAIApi {
impl ApiDefinition for OpenAIApi {
fn endpoint(&self) -> &'static str {
match self {
OpenAIApi::ChatCompletions => "/v1/chat/completions",
OpenAIApi::ChatCompletions => CHAT_COMPLETIONS_PATH,
}
}
fn from_endpoint(endpoint: &str) -> Option<Self> {
match endpoint {
"/v1/chat/completions" => Some(OpenAIApi::ChatCompletions),
CHAT_COMPLETIONS_PATH => Some(OpenAIApi::ChatCompletions),
_ => None,
}
}
@ -943,13 +944,13 @@ mod tests {
let api = OpenAIApi::ChatCompletions;
// Test trait methods
assert_eq!(api.endpoint(), "/v1/chat/completions");
assert_eq!(api.endpoint(), CHAT_COMPLETIONS_PATH);
assert!(api.supports_streaming());
assert!(api.supports_tools());
assert!(api.supports_vision());
// Test from_endpoint
let found_api = OpenAIApi::from_endpoint("/v1/chat/completions");
let found_api = OpenAIApi::from_endpoint(CHAT_COMPLETIONS_PATH);
assert_eq!(found_api, Some(OpenAIApi::ChatCompletions));
let not_found = OpenAIApi::from_endpoint("/v1/unknown");

View file

@ -4,13 +4,18 @@
pub mod providers;
pub mod apis;
pub mod clients;
// Re-export important types and traits
pub use providers::request::{ProviderRequestType, ProviderRequest, ProviderRequestError};
pub use providers::response::{ProviderResponseType, ProviderStreamResponseType, ProviderResponse, ProviderStreamResponse, ProviderResponseError, TokenUsage, SseEvent, SseStreamIter};
pub use providers::id::ProviderId;
pub use providers::adapters::{has_compatible_api, supported_apis};
//TODO: Refactor such that commons doesn't depend on Hermes. For now this will clean up strings
pub const CHAT_COMPLETIONS_PATH: &str = "/v1/chat/completions";
pub const MESSAGES_PATH: &str = "/v1/messages";
#[cfg(test)]
mod tests {
use super::*;
@ -25,17 +30,17 @@ mod tests {
#[test]
fn test_provider_api_compatibility() {
assert!(has_compatible_api(&ProviderId::OpenAI, "/v1/chat/completions"));
assert!(has_compatible_api(&ProviderId::OpenAI, CHAT_COMPLETIONS_PATH));
assert!(!has_compatible_api(&ProviderId::OpenAI, "/v1/embeddings"));
}
#[test]
fn test_provider_supported_apis() {
let apis = supported_apis(&ProviderId::OpenAI);
assert!(apis.contains(&"/v1/chat/completions"));
assert!(apis.contains(&CHAT_COMPLETIONS_PATH));
// Test that provider supports the expected API endpoints
assert!(has_compatible_api(&ProviderId::OpenAI, "/v1/chat/completions"));
assert!(has_compatible_api(&ProviderId::OpenAI, CHAT_COMPLETIONS_PATH));
}
#[test]

View file

@ -1,15 +1,5 @@
//! Provider adapter configuration and API compatibility utilities.
//
// Note: For all request/response conversions between Anthropic and OpenAI APIs,
// use the peer-reviewed and well-tested implementations in `clients/transformer.rs`.
// This file should not contain conversion logic.
/// Utility to check if a model is from the Claude/Anthropic family
pub fn is_claude_family(model: &str) -> bool {
let model = model.to_lowercase();
model.contains("claude") || model.contains("anthropic")
}
use crate::providers::id::ProviderId;
use crate::{CHAT_COMPLETIONS_PATH, MESSAGES_PATH};
#[derive(Debug, Clone)]
pub enum AdapterType {
@ -43,13 +33,13 @@ pub fn get_provider_config(provider_id: &ProviderId) -> ProviderConfig {
ProviderId::OpenAI | ProviderId::Groq | ProviderId::Mistral | ProviderId::Deepseek
| ProviderId::Arch | ProviderId::Gemini | ProviderId::GitHub => {
ProviderConfig {
supported_apis: &["/v1/chat/completions"],
supported_apis: &[CHAT_COMPLETIONS_PATH],
adapter_type: AdapterType::OpenAICompatible,
}
}
ProviderId::Anthropic => {
ProviderConfig {
supported_apis: &["/v1/messages", "/v1/chat/completions"],
supported_apis: &[MESSAGES_PATH],
adapter_type: AdapterType::AnthropicCompatible,
}
}

View file

@ -148,31 +148,32 @@ impl fmt::Display for SseEvent {
}
}
// Into implementation to convert SseEvent to bytes for response buffer
impl Into<Vec<u8>> for SseEvent {
fn into(self) -> Vec<u8> {
format!("{}\n\n", self.raw_line).into_bytes()
}
}
// --- Response transformation logic for client API compatibility ---
impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType {
type Error = std::io::Error;
fn try_from((bytes, client_api, provider_id): (&[u8], &SupportedAPIs, &ProviderId)) -> Result<Self, Self::Error> {
let upstream_api = provider_id.compatible_api_for_client(client_api);
// Step 1: Parse bytes using upstream API format (what the provider actually sent)
// Step 2: Return response type that matches client API format (what client expects)
match (&upstream_api, client_api) {
// Upstream sent OpenAI format, client expects OpenAI format - direct pass-through
(SupportedAPIs::OpenAIChatCompletions(_), SupportedAPIs::OpenAIChatCompletions(_)) => {
let resp: ChatCompletionsResponse = ChatCompletionsResponse::try_from(bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(ProviderResponseType::ChatCompletionsResponse(resp))
}
// Upstream sent Anthropic format, client expects Anthropic format - direct pass-through
(SupportedAPIs::AnthropicMessagesAPI(_), SupportedAPIs::AnthropicMessagesAPI(_)) => {
let resp: MessagesResponse = serde_json::from_slice(bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(ProviderResponseType::MessagesResponse(resp))
}
// Upstream sent Anthropic format, client expects OpenAI format - need transformation
(SupportedAPIs::AnthropicMessagesAPI(_), SupportedAPIs::OpenAIChatCompletions(_)) => {
// Parse as Anthropic Messages response first
let anthropic_resp: MessagesResponse = serde_json::from_slice(bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
@ -181,9 +182,7 @@ impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType {
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Transformation error: {}", e)))?;
Ok(ProviderResponseType::ChatCompletionsResponse(chat_resp))
}
// Upstream sent OpenAI format, client expects Anthropic format - need transformation
(SupportedAPIs::OpenAIChatCompletions(_), SupportedAPIs::AnthropicMessagesAPI(_)) => {
// Parse as OpenAI ChatCompletions response first
let openai_resp: ChatCompletionsResponse = ChatCompletionsResponse::try_from(bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
@ -202,32 +201,23 @@ impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderStreamResponseTyp
fn try_from((bytes, client_api, provider_id): (&[u8], &SupportedAPIs, &ProviderId)) -> Result<Self, Self::Error> {
let upstream_api = provider_id.compatible_api_for_client(client_api);
// Step 1: Parse bytes using upstream API format (what the provider actually sent)
// Step 2: Return response type that matches client API format (what client expects)
match (&upstream_api, client_api) {
// Upstream sent OpenAI format, client expects OpenAI format - direct pass-through
(SupportedAPIs::OpenAIChatCompletions(_), SupportedAPIs::OpenAIChatCompletions(_)) => {
let resp: crate::apis::openai::ChatCompletionsStreamResponse = serde_json::from_slice(bytes)?;
Ok(ProviderStreamResponseType::ChatCompletionsStreamResponse(resp))
}
// Upstream sent Anthropic format, client expects Anthropic format - direct pass-through
(SupportedAPIs::AnthropicMessagesAPI(_), SupportedAPIs::AnthropicMessagesAPI(_)) => {
let resp: crate::apis::anthropic::MessagesStreamEvent = serde_json::from_slice(bytes)?;
Ok(ProviderStreamResponseType::MessagesStreamEvent(resp))
}
// Upstream sent Anthropic format, client expects OpenAI format - need transformation
(SupportedAPIs::AnthropicMessagesAPI(_), SupportedAPIs::OpenAIChatCompletions(_)) => {
// Parse as Anthropic Messages stream event first
let anthropic_resp: crate::apis::anthropic::MessagesStreamEvent = serde_json::from_slice(bytes)?;
// Transform to OpenAI ChatCompletions stream format using the transformer
let chat_resp: crate::apis::openai::ChatCompletionsStreamResponse = anthropic_resp.try_into()?;
Ok(ProviderStreamResponseType::ChatCompletionsStreamResponse(chat_resp))
}
// Upstream sent OpenAI format, client expects Anthropic format - need transformation
(SupportedAPIs::OpenAIChatCompletions(_), SupportedAPIs::AnthropicMessagesAPI(_)) => {
// Parse as OpenAI ChatCompletions stream response first
let openai_resp: crate::apis::openai::ChatCompletionsStreamResponse = serde_json::from_slice(bytes)?;
// Transform to Anthropic Messages stream format using the transformer
@ -304,12 +294,6 @@ impl TryFrom<(SseEvent, &SupportedAPIs, &SupportedAPIs)> for SseEvent {
}
}
// Into implementation to convert SseEvent to bytes for response buffer
impl Into<Vec<u8>> for SseEvent {
fn into(self) -> Vec<u8> {
format!("{}\n\n", self.raw_line).into_bytes()
}
}
#[derive(Debug)]