diff --git a/crates/brightstaff/src/main.rs b/crates/brightstaff/src/main.rs index 4f5357eb..d3843125 100644 --- a/crates/brightstaff/src/main.rs +++ b/crates/brightstaff/src/main.rs @@ -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> { 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) diff --git a/crates/hermesllm/src/apis/anthropic.rs b/crates/hermesllm/src/apis/anthropic.rs index d83e4968..db166f3f 100644 --- a/crates/hermesllm/src/apis/anthropic.rs +++ b/crates/hermesllm/src/apis/anthropic.rs @@ -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 { 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"); diff --git a/crates/hermesllm/src/apis/mod.rs b/crates/hermesllm/src/apis/mod.rs index 78b634d5..b175988c 100644 --- a/crates/hermesllm/src/apis/mod.rs +++ b/crates/hermesllm/src/apis/mod.rs @@ -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 { -/// 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::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(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" ]; diff --git a/crates/hermesllm/src/apis/openai.rs b/crates/hermesllm/src/apis/openai.rs index 287b1cde..ad65a55d 100644 --- a/crates/hermesllm/src/apis/openai.rs +++ b/crates/hermesllm/src/apis/openai.rs @@ -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 { 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"); diff --git a/crates/hermesllm/src/lib.rs b/crates/hermesllm/src/lib.rs index c8167d89..e552102a 100644 --- a/crates/hermesllm/src/lib.rs +++ b/crates/hermesllm/src/lib.rs @@ -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] diff --git a/crates/hermesllm/src/providers/adapters.rs b/crates/hermesllm/src/providers/adapters.rs index 09bf7108..4cf918b1 100644 --- a/crates/hermesllm/src/providers/adapters.rs +++ b/crates/hermesllm/src/providers/adapters.rs @@ -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, } } diff --git a/crates/hermesllm/src/providers/response.rs b/crates/hermesllm/src/providers/response.rs index 077d54ba..119cbc34 100644 --- a/crates/hermesllm/src/providers/response.rs +++ b/crates/hermesllm/src/providers/response.rs @@ -148,31 +148,32 @@ impl fmt::Display for SseEvent { } } +// Into implementation to convert SseEvent to bytes for response buffer +impl Into> for SseEvent { + fn into(self) -> Vec { + 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 { 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 { 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> for SseEvent { - fn into(self) -> Vec { - format!("{}\n\n", self.raw_line).into_bytes() - } -} #[derive(Debug)]