2025-10-22 11:31:21 -07:00
|
|
|
use crate::apis::{AmazonBedrockApi, AnthropicApi, ApiDefinition, OpenAIApi};
|
|
|
|
|
use crate::ProviderId;
|
2025-09-10 07:40:30 -07:00
|
|
|
use std::fmt;
|
2025-08-07 12:42:09 -07:00
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
/// Unified enum representing all supported API endpoints across providers
|
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
2025-12-03 14:58:26 -08:00
|
|
|
pub enum SupportedAPIsFromClient {
|
2025-09-10 07:40:30 -07:00
|
|
|
OpenAIChatCompletions(OpenAIApi),
|
|
|
|
|
AnthropicMessagesAPI(AnthropicApi),
|
2025-12-03 14:58:26 -08:00
|
|
|
OpenAIResponsesAPI(OpenAIApi),
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
|
2025-10-22 11:31:21 -07:00
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum SupportedUpstreamAPIs {
|
|
|
|
|
OpenAIChatCompletions(OpenAIApi),
|
|
|
|
|
AnthropicMessagesAPI(AnthropicApi),
|
|
|
|
|
AmazonBedrockConverse(AmazonBedrockApi),
|
|
|
|
|
AmazonBedrockConverseStream(AmazonBedrockApi),
|
2025-12-03 14:58:26 -08:00
|
|
|
OpenAIResponsesAPI(OpenAIApi),
|
2025-10-22 11:31:21 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
impl fmt::Display for SupportedAPIsFromClient {
|
2025-09-10 07:40:30 -07:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
match self {
|
2025-12-03 14:58:26 -08:00
|
|
|
SupportedAPIsFromClient::OpenAIChatCompletions(api) => {
|
2025-10-24 14:07:05 -07:00
|
|
|
write!(f, "OpenAI ({})", api.endpoint())
|
2025-10-14 14:01:11 -07:00
|
|
|
}
|
2025-12-03 14:58:26 -08:00
|
|
|
SupportedAPIsFromClient::AnthropicMessagesAPI(api) => {
|
2025-10-24 14:07:05 -07:00
|
|
|
write!(f, "Anthropic AI ({})", api.endpoint())
|
|
|
|
|
}
|
2025-12-03 14:58:26 -08:00
|
|
|
SupportedAPIsFromClient::OpenAIResponsesAPI(api) => {
|
|
|
|
|
write!(f, "OpenAI Responses ({})", api.endpoint())
|
|
|
|
|
}
|
2025-10-24 14:07:05 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for SupportedUpstreamAPIs {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
SupportedUpstreamAPIs::OpenAIChatCompletions(api) => {
|
|
|
|
|
write!(f, "OpenAI ({})", api.endpoint())
|
|
|
|
|
}
|
|
|
|
|
SupportedUpstreamAPIs::AnthropicMessagesAPI(api) => {
|
|
|
|
|
write!(f, "Anthropic ({})", api.endpoint())
|
|
|
|
|
}
|
|
|
|
|
SupportedUpstreamAPIs::AmazonBedrockConverse(api) => {
|
|
|
|
|
write!(f, "Amazon Bedrock ({})", api.endpoint())
|
|
|
|
|
}
|
|
|
|
|
SupportedUpstreamAPIs::AmazonBedrockConverseStream(api) => {
|
|
|
|
|
write!(f, "Amazon Bedrock ({})", api.endpoint())
|
2025-10-14 14:01:11 -07:00
|
|
|
}
|
2025-12-03 14:58:26 -08:00
|
|
|
SupportedUpstreamAPIs::OpenAIResponsesAPI(api) => {
|
|
|
|
|
write!(f, "OpenAI Responses ({})", api.endpoint())
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
2025-08-07 12:42:09 -07:00
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
2025-08-07 12:42:09 -07:00
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
impl SupportedAPIsFromClient {
|
2025-09-10 07:40:30 -07:00
|
|
|
/// Create a SupportedApi from an endpoint path
|
|
|
|
|
pub fn from_endpoint(endpoint: &str) -> Option<Self> {
|
|
|
|
|
if let Some(openai_api) = OpenAIApi::from_endpoint(endpoint) {
|
2025-12-03 14:58:26 -08:00
|
|
|
// Check if this is the Responses API endpoint
|
|
|
|
|
if openai_api == OpenAIApi::Responses {
|
|
|
|
|
return Some(SupportedAPIsFromClient::OpenAIResponsesAPI(openai_api));
|
|
|
|
|
}
|
|
|
|
|
// Otherwise it's ChatCompletions
|
|
|
|
|
return Some(SupportedAPIsFromClient::OpenAIChatCompletions(openai_api));
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(anthropic_api) = AnthropicApi::from_endpoint(endpoint) {
|
2025-12-03 14:58:26 -08:00
|
|
|
return Some(SupportedAPIsFromClient::AnthropicMessagesAPI(anthropic_api));
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the endpoint path for this API
|
|
|
|
|
pub fn endpoint(&self) -> &'static str {
|
|
|
|
|
match self {
|
2025-12-03 14:58:26 -08:00
|
|
|
SupportedAPIsFromClient::OpenAIChatCompletions(api) => api.endpoint(),
|
|
|
|
|
SupportedAPIsFromClient::AnthropicMessagesAPI(api) => api.endpoint(),
|
|
|
|
|
SupportedAPIsFromClient::OpenAIResponsesAPI(api) => api.endpoint(),
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
2025-08-07 12:42:09 -07:00
|
|
|
}
|
|
|
|
|
|
2025-10-14 14:01:11 -07:00
|
|
|
pub fn target_endpoint_for_provider(
|
|
|
|
|
&self,
|
|
|
|
|
provider_id: &ProviderId,
|
|
|
|
|
request_path: &str,
|
|
|
|
|
model_id: &str,
|
2025-10-22 11:31:21 -07:00
|
|
|
is_streaming: bool,
|
2025-10-29 17:08:07 -07:00
|
|
|
base_url_path_prefix: Option<&str>,
|
2025-10-14 14:01:11 -07:00
|
|
|
) -> String {
|
2025-10-29 17:08:07 -07:00
|
|
|
// Helper function to build endpoint with optional prefix override
|
|
|
|
|
let build_endpoint = |provider_prefix: &str, suffix: &str| -> String {
|
|
|
|
|
let prefix = base_url_path_prefix
|
|
|
|
|
.map(|p| p.trim_matches('/'))
|
|
|
|
|
.filter(|p| !p.is_empty())
|
|
|
|
|
.unwrap_or(provider_prefix.trim_matches('/'));
|
|
|
|
|
|
|
|
|
|
let suffix = suffix.trim_start_matches('/');
|
|
|
|
|
if prefix.is_empty() {
|
|
|
|
|
format!("/{}", suffix)
|
|
|
|
|
} else {
|
|
|
|
|
format!("/{}/{}", prefix, suffix)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
// Helper function to route based on provider with a specific endpoint suffix
|
|
|
|
|
let route_by_provider = |endpoint_suffix: &str| -> String {
|
|
|
|
|
match provider_id {
|
2025-10-14 14:01:11 -07:00
|
|
|
ProviderId::Groq => {
|
|
|
|
|
if request_path.starts_with("/v1/") {
|
2025-10-29 17:08:07 -07:00
|
|
|
build_endpoint("/openai", request_path)
|
2025-10-14 14:01:11 -07:00
|
|
|
} else {
|
2025-12-03 14:58:26 -08:00
|
|
|
build_endpoint("/v1", endpoint_suffix)
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
2025-10-14 14:01:11 -07:00
|
|
|
}
|
|
|
|
|
ProviderId::Zhipu => {
|
|
|
|
|
if request_path.starts_with("/v1/") {
|
2025-12-03 14:58:26 -08:00
|
|
|
build_endpoint("/api/paas/v4", endpoint_suffix)
|
2025-10-14 14:01:11 -07:00
|
|
|
} else {
|
2025-12-03 14:58:26 -08:00
|
|
|
build_endpoint("/v1", endpoint_suffix)
|
2025-09-30 12:24:06 -07:00
|
|
|
}
|
2025-10-14 14:01:11 -07:00
|
|
|
}
|
|
|
|
|
ProviderId::Qwen => {
|
|
|
|
|
if request_path.starts_with("/v1/") {
|
2025-12-03 14:58:26 -08:00
|
|
|
build_endpoint("/compatible-mode/v1", endpoint_suffix)
|
2025-10-14 14:01:11 -07:00
|
|
|
} else {
|
2025-12-03 14:58:26 -08:00
|
|
|
build_endpoint("/v1", endpoint_suffix)
|
2025-10-01 21:57:58 -07:00
|
|
|
}
|
2025-10-14 14:01:11 -07:00
|
|
|
}
|
|
|
|
|
ProviderId::AzureOpenAI => {
|
|
|
|
|
if request_path.starts_with("/v1/") {
|
2025-12-03 14:58:26 -08:00
|
|
|
let suffix = endpoint_suffix.trim_start_matches('/');
|
|
|
|
|
build_endpoint("/openai/deployments", &format!("/{}/{}?api-version=2025-01-01-preview", model_id, suffix))
|
2025-10-14 14:01:11 -07:00
|
|
|
} else {
|
2025-12-03 14:58:26 -08:00
|
|
|
build_endpoint("/v1", endpoint_suffix)
|
2025-09-18 18:36:30 -07:00
|
|
|
}
|
2025-10-14 14:01:11 -07:00
|
|
|
}
|
|
|
|
|
ProviderId::Gemini => {
|
|
|
|
|
if request_path.starts_with("/v1/") {
|
2025-12-03 14:58:26 -08:00
|
|
|
build_endpoint("/v1beta/openai", endpoint_suffix)
|
2025-10-14 14:01:11 -07:00
|
|
|
} else {
|
2025-12-03 14:58:26 -08:00
|
|
|
build_endpoint("/v1", endpoint_suffix)
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-22 11:31:21 -07:00
|
|
|
ProviderId::AmazonBedrock => {
|
|
|
|
|
if request_path.starts_with("/v1/") {
|
|
|
|
|
if !is_streaming {
|
2025-10-29 17:08:07 -07:00
|
|
|
build_endpoint("", &format!("/model/{}/converse", model_id))
|
2025-10-22 11:31:21 -07:00
|
|
|
} else {
|
2025-10-29 17:08:07 -07:00
|
|
|
build_endpoint("", &format!("/model/{}/converse-stream", model_id))
|
2025-10-22 11:31:21 -07:00
|
|
|
}
|
2025-12-03 14:58:26 -08:00
|
|
|
} else {
|
|
|
|
|
build_endpoint("/v1", endpoint_suffix)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => build_endpoint("/v1", endpoint_suffix),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match self {
|
|
|
|
|
SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages) => match provider_id {
|
|
|
|
|
ProviderId::Anthropic => build_endpoint("/v1", "/messages"),
|
|
|
|
|
ProviderId::AmazonBedrock => {
|
|
|
|
|
if request_path.starts_with("/v1/") && !is_streaming {
|
|
|
|
|
build_endpoint("", &format!("/model/{}/converse", model_id))
|
|
|
|
|
} else if request_path.starts_with("/v1/") && is_streaming {
|
|
|
|
|
build_endpoint("", &format!("/model/{}/converse-stream", model_id))
|
2025-10-22 11:31:21 -07:00
|
|
|
} else {
|
2025-10-29 17:08:07 -07:00
|
|
|
build_endpoint("/v1", "/chat/completions")
|
2025-10-22 11:31:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-29 17:08:07 -07:00
|
|
|
_ => build_endpoint("/v1", "/chat/completions"),
|
2025-10-14 14:01:11 -07:00
|
|
|
},
|
2025-12-03 14:58:26 -08:00
|
|
|
SupportedAPIsFromClient::OpenAIResponsesAPI(_) => {
|
|
|
|
|
// For Responses API, check if provider supports it, otherwise translate to chat/completions
|
|
|
|
|
match provider_id {
|
|
|
|
|
// OpenAI and compatible providers that support /v1/responses
|
|
|
|
|
ProviderId::OpenAI => route_by_provider("/responses"),
|
|
|
|
|
// All other providers: translate to /chat/completions
|
|
|
|
|
_ => route_by_provider("/chat/completions"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
SupportedAPIsFromClient::OpenAIChatCompletions(_) => {
|
|
|
|
|
// For Chat Completions API, use the standard chat/completions path
|
|
|
|
|
route_by_provider("/chat/completions")
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-07 12:42:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get all supported endpoint paths
|
|
|
|
|
pub fn supported_endpoints() -> Vec<&'static str> {
|
|
|
|
|
let mut endpoints = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Add all OpenAI endpoints
|
|
|
|
|
for api in OpenAIApi::all_variants() {
|
|
|
|
|
endpoints.push(api.endpoint());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add all Anthropic endpoints
|
|
|
|
|
for api in AnthropicApi::all_variants() {
|
|
|
|
|
endpoints.push(api.endpoint());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
endpoints
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Identify which provider supports a given endpoint
|
|
|
|
|
pub fn identify_provider(endpoint: &str) -> Option<&'static str> {
|
|
|
|
|
if OpenAIApi::from_endpoint(endpoint).is_some() {
|
|
|
|
|
return Some("openai");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if AnthropicApi::from_endpoint(endpoint).is_some() {
|
|
|
|
|
return Some("anthropic");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_is_supported_endpoint() {
|
|
|
|
|
// OpenAI endpoints
|
2025-12-03 14:58:26 -08:00
|
|
|
assert!(SupportedAPIsFromClient::from_endpoint("/v1/chat/completions").is_some());
|
2025-08-07 12:42:09 -07:00
|
|
|
// Anthropic endpoints
|
2025-12-03 14:58:26 -08:00
|
|
|
assert!(SupportedAPIsFromClient::from_endpoint("/v1/messages").is_some());
|
2025-08-07 12:42:09 -07:00
|
|
|
|
|
|
|
|
// Unsupported endpoints
|
2025-12-03 14:58:26 -08:00
|
|
|
assert!(!SupportedAPIsFromClient::from_endpoint("/v1/unknown").is_some());
|
|
|
|
|
assert!(!SupportedAPIsFromClient::from_endpoint("/v2/chat").is_some());
|
|
|
|
|
assert!(!SupportedAPIsFromClient::from_endpoint("").is_some());
|
2025-08-07 12:42:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_supported_endpoints() {
|
|
|
|
|
let endpoints = supported_endpoints();
|
2025-12-03 14:58:26 -08:00
|
|
|
assert_eq!(endpoints.len(), 3); // We have 3 APIs defined
|
2025-08-07 12:42:09 -07:00
|
|
|
assert!(endpoints.contains(&"/v1/chat/completions"));
|
|
|
|
|
assert!(endpoints.contains(&"/v1/messages"));
|
2025-12-03 14:58:26 -08:00
|
|
|
assert!(endpoints.contains(&"/v1/responses"));
|
2025-08-07 12:42:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_identify_provider() {
|
|
|
|
|
assert_eq!(identify_provider("/v1/chat/completions"), Some("openai"));
|
|
|
|
|
assert_eq!(identify_provider("/v1/messages"), Some("anthropic"));
|
|
|
|
|
assert_eq!(identify_provider("/v1/unknown"), None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_endpoints_generated_from_api_definitions() {
|
|
|
|
|
let endpoints = supported_endpoints();
|
|
|
|
|
|
|
|
|
|
// Verify that we get endpoints from all API variants
|
|
|
|
|
let openai_endpoints: Vec<_> = OpenAIApi::all_variants()
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|api| api.endpoint())
|
|
|
|
|
.collect();
|
|
|
|
|
let anthropic_endpoints: Vec<_> = AnthropicApi::all_variants()
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|api| api.endpoint())
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// All OpenAI endpoints should be in the result
|
|
|
|
|
for endpoint in openai_endpoints {
|
2025-10-14 14:01:11 -07:00
|
|
|
assert!(
|
|
|
|
|
endpoints.contains(&endpoint),
|
|
|
|
|
"Missing OpenAI endpoint: {}",
|
|
|
|
|
endpoint
|
|
|
|
|
);
|
2025-08-07 12:42:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All Anthropic endpoints should be in the result
|
|
|
|
|
for endpoint in anthropic_endpoints {
|
2025-10-14 14:01:11 -07:00
|
|
|
assert!(
|
|
|
|
|
endpoints.contains(&endpoint),
|
|
|
|
|
"Missing Anthropic endpoint: {}",
|
|
|
|
|
endpoint
|
|
|
|
|
);
|
2025-08-07 12:42:09 -07:00
|
|
|
}
|
|
|
|
|
// Total should match
|
2025-10-14 14:01:11 -07:00
|
|
|
assert_eq!(
|
|
|
|
|
endpoints.len(),
|
|
|
|
|
OpenAIApi::all_variants().len() + AnthropicApi::all_variants().len()
|
|
|
|
|
);
|
2025-08-07 12:42:09 -07:00
|
|
|
}
|
2025-10-29 17:08:07 -07:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_target_endpoint_without_base_url_prefix() {
|
2025-12-03 14:58:26 -08:00
|
|
|
let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions);
|
2025-10-29 17:08:07 -07:00
|
|
|
|
|
|
|
|
// Test default OpenAI provider
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::OpenAI,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"gpt-4",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/v1/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Groq provider
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Groq,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"llama2",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/openai/v1/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Zhipu provider
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Zhipu,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"chatglm",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/api/paas/v4/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Qwen provider
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Qwen,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"qwen-turbo",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/compatible-mode/v1/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Azure OpenAI provider
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::AzureOpenAI,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"gpt-4",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/openai/deployments/gpt-4/chat/completions?api-version=2025-01-01-preview"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Gemini provider
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Gemini,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"gemini-pro",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/v1beta/openai/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_target_endpoint_with_base_url_prefix() {
|
2025-12-03 14:58:26 -08:00
|
|
|
let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions);
|
2025-10-29 17:08:07 -07:00
|
|
|
|
|
|
|
|
// Test Zhipu with custom base_url_path_prefix
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Zhipu,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"chatglm",
|
|
|
|
|
false,
|
|
|
|
|
Some("/api/coding/paas/v4")
|
|
|
|
|
),
|
|
|
|
|
"/api/coding/paas/v4/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test with prefix without leading slash
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Zhipu,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"chatglm",
|
|
|
|
|
false,
|
|
|
|
|
Some("api/coding/paas/v4")
|
|
|
|
|
),
|
|
|
|
|
"/api/coding/paas/v4/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test with prefix with trailing slash
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Zhipu,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"chatglm",
|
|
|
|
|
false,
|
|
|
|
|
Some("/api/coding/paas/v4/")
|
|
|
|
|
),
|
|
|
|
|
"/api/coding/paas/v4/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test OpenAI with custom prefix
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::OpenAI,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"gpt-4",
|
|
|
|
|
false,
|
|
|
|
|
Some("/custom/api/v2")
|
|
|
|
|
),
|
|
|
|
|
"/custom/api/v2/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Groq with custom prefix
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Groq,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"llama2",
|
|
|
|
|
false,
|
|
|
|
|
Some("/api/v2")
|
|
|
|
|
),
|
|
|
|
|
"/api/v2/v1/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_target_endpoint_with_empty_base_url_prefix() {
|
2025-12-03 14:58:26 -08:00
|
|
|
let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions);
|
2025-10-29 17:08:07 -07:00
|
|
|
|
|
|
|
|
// Test with just slashes - trims to empty, uses provider default
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Zhipu,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"chatglm",
|
|
|
|
|
false,
|
|
|
|
|
Some("/")
|
|
|
|
|
),
|
|
|
|
|
"/api/paas/v4/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test with None - uses provider default
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Zhipu,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"chatglm",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/api/paas/v4/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_amazon_bedrock_endpoints() {
|
2025-12-03 14:58:26 -08:00
|
|
|
let api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages);
|
2025-10-29 17:08:07 -07:00
|
|
|
|
|
|
|
|
// Test Bedrock non-streaming without prefix
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::AmazonBedrock,
|
|
|
|
|
"/v1/messages",
|
|
|
|
|
"us.amazon.nova-pro-v1:0",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/model/us.amazon.nova-pro-v1:0/converse"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Bedrock streaming without prefix
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::AmazonBedrock,
|
|
|
|
|
"/v1/messages",
|
|
|
|
|
"us.amazon.nova-pro-v1:0",
|
|
|
|
|
true,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/model/us.amazon.nova-pro-v1:0/converse-stream"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Bedrock non-streaming with prefix (prefix shouldn't affect bedrock)
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::AmazonBedrock,
|
|
|
|
|
"/v1/messages",
|
|
|
|
|
"us.amazon.nova-pro-v1:0",
|
|
|
|
|
false,
|
|
|
|
|
Some("/custom/path")
|
|
|
|
|
),
|
|
|
|
|
"/custom/path/model/us.amazon.nova-pro-v1:0/converse"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Bedrock streaming with prefix
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::AmazonBedrock,
|
|
|
|
|
"/v1/messages",
|
|
|
|
|
"us.amazon.nova-pro-v1:0",
|
|
|
|
|
true,
|
|
|
|
|
Some("/custom/path")
|
|
|
|
|
),
|
|
|
|
|
"/custom/path/model/us.amazon.nova-pro-v1:0/converse-stream"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_anthropic_messages_endpoint() {
|
2025-12-03 14:58:26 -08:00
|
|
|
let api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages);
|
2025-10-29 17:08:07 -07:00
|
|
|
|
|
|
|
|
// Test Anthropic without prefix
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Anthropic,
|
|
|
|
|
"/v1/messages",
|
|
|
|
|
"claude-3-opus",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/v1/messages"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Anthropic with prefix
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Anthropic,
|
|
|
|
|
"/v1/messages",
|
|
|
|
|
"claude-3-opus",
|
|
|
|
|
false,
|
|
|
|
|
Some("/api/v2")
|
|
|
|
|
),
|
|
|
|
|
"/api/v2/messages"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_non_v1_request_paths() {
|
2025-12-03 14:58:26 -08:00
|
|
|
let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions);
|
2025-10-29 17:08:07 -07:00
|
|
|
|
|
|
|
|
// Test Groq with non-v1 path (should use default)
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Groq,
|
|
|
|
|
"/custom/path",
|
|
|
|
|
"llama2",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/v1/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Zhipu with non-v1 path
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Zhipu,
|
|
|
|
|
"/custom/path",
|
|
|
|
|
"chatglm",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/v1/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test with prefix on non-v1 path
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::Zhipu,
|
|
|
|
|
"/custom/path",
|
|
|
|
|
"chatglm",
|
|
|
|
|
false,
|
|
|
|
|
Some("/api/v2")
|
|
|
|
|
),
|
|
|
|
|
"/api/v2/chat/completions"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_azure_openai_with_query_params() {
|
2025-12-03 14:58:26 -08:00
|
|
|
let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions);
|
2025-10-29 17:08:07 -07:00
|
|
|
|
|
|
|
|
// Test Azure without prefix - should include query params
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::AzureOpenAI,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"gpt-4-deployment",
|
|
|
|
|
false,
|
|
|
|
|
None
|
|
|
|
|
),
|
|
|
|
|
"/openai/deployments/gpt-4-deployment/chat/completions?api-version=2025-01-01-preview"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Azure with prefix - prefix should replace /openai/deployments
|
|
|
|
|
assert_eq!(
|
|
|
|
|
api.target_endpoint_for_provider(
|
|
|
|
|
&ProviderId::AzureOpenAI,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
"gpt-4-deployment",
|
|
|
|
|
false,
|
|
|
|
|
Some("/custom/azure/path")
|
|
|
|
|
),
|
|
|
|
|
"/custom/azure/path/gpt-4-deployment/chat/completions?api-version=2025-01-01-preview"
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-07 12:42:09 -07:00
|
|
|
}
|