mirror of
https://github.com/katanemo/plano.git
synced 2026-06-17 15:25:17 +02:00
moved away from string literals to consts
This commit is contained in:
parent
2de75d18db
commit
06c71c1392
7 changed files with 36 additions and 155 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue