Add support for v1/responses API (#622)

* making first commit. still need to work on streaming respones

* making first commit. still need to work on streaming respones

* stream buffer implementation with tests

* adding grok API keys to workflow

* fixed changes based on code review

* adding support for bedrock models

* fixed issues with translation to claude code

---------

Co-authored-by: Salman Paracha <salmanparacha@MacBook-Pro-342.local>
This commit is contained in:
Salman Paracha 2025-12-03 14:58:26 -08:00 committed by GitHub
parent b01a81927d
commit a448c6e9cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 7015 additions and 2955 deletions

View file

@ -1,5 +1,5 @@
use crate::apis::{AmazonBedrockApi, AnthropicApi, OpenAIApi};
use crate::clients::endpoints::{SupportedAPIs, SupportedUpstreamAPIs};
use crate::clients::endpoints::{SupportedAPIsFromClient, SupportedUpstreamAPIs};
use std::fmt::Display;
/// Provider identifier enum - simple enum for identifying providers
@ -51,19 +51,24 @@ impl ProviderId {
/// Given a client API, return the compatible upstream API for this provider
pub fn compatible_api_for_client(
&self,
client_api: &SupportedAPIs,
client_api: &SupportedAPIsFromClient,
is_streaming: bool,
) -> SupportedUpstreamAPIs {
match (self, client_api) {
// Claude/Anthropic providers natively support Anthropic APIs
(ProviderId::Anthropic, SupportedAPIs::AnthropicMessagesAPI(_)) => {
(ProviderId::Anthropic, SupportedAPIsFromClient::AnthropicMessagesAPI(_)) => {
SupportedUpstreamAPIs::AnthropicMessagesAPI(AnthropicApi::Messages)
}
(
ProviderId::Anthropic,
SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions),
SupportedAPIsFromClient::OpenAIChatCompletions(_),
) => SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions),
// Anthropic doesn't support Responses API, fall back to chat completions
(ProviderId::Anthropic, SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => {
SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions)
}
// OpenAI-compatible providers only support OpenAI chat completions
(
ProviderId::OpenAI
@ -80,7 +85,7 @@ impl ProviderId {
| ProviderId::Moonshotai
| ProviderId::Zhipu
| ProviderId::Qwen,
SupportedAPIs::AnthropicMessagesAPI(_),
SupportedAPIsFromClient::AnthropicMessagesAPI(_),
) => SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions),
(
@ -98,11 +103,16 @@ impl ProviderId {
| ProviderId::Moonshotai
| ProviderId::Zhipu
| ProviderId::Qwen,
SupportedAPIs::OpenAIChatCompletions(_),
SupportedAPIsFromClient::OpenAIChatCompletions(_),
) => SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions),
// OpenAI Responses API - only OpenAI supports this
(ProviderId::OpenAI, SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => {
SupportedUpstreamAPIs::OpenAIResponsesAPI(OpenAIApi::Responses)
}
// Amazon Bedrock natively supports Bedrock APIs
(ProviderId::AmazonBedrock, SupportedAPIs::OpenAIChatCompletions(_)) => {
(ProviderId::AmazonBedrock, SupportedAPIsFromClient::OpenAIChatCompletions(_)) => {
if is_streaming {
SupportedUpstreamAPIs::AmazonBedrockConverseStream(
AmazonBedrockApi::ConverseStream,
@ -111,7 +121,7 @@ impl ProviderId {
SupportedUpstreamAPIs::AmazonBedrockConverse(AmazonBedrockApi::Converse)
}
}
(ProviderId::AmazonBedrock, SupportedAPIs::AnthropicMessagesAPI(_)) => {
(ProviderId::AmazonBedrock, SupportedAPIsFromClient::AnthropicMessagesAPI(_)) => {
if is_streaming {
SupportedUpstreamAPIs::AmazonBedrockConverseStream(
AmazonBedrockApi::ConverseStream,
@ -120,6 +130,20 @@ impl ProviderId {
SupportedUpstreamAPIs::AmazonBedrockConverse(AmazonBedrockApi::Converse)
}
}
(ProviderId::AmazonBedrock, SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => {
if is_streaming {
SupportedUpstreamAPIs::AmazonBedrockConverseStream(
AmazonBedrockApi::ConverseStream,
)
} else {
SupportedUpstreamAPIs::AmazonBedrockConverse(AmazonBedrockApi::Converse)
}
}
// Non-OpenAI providers: if client requested the Responses API, fall back to Chat Completions
(_, SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => {
SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions)
}
}
}
}

View file

@ -6,7 +6,9 @@
pub mod id;
pub mod request;
pub mod response;
pub mod streaming_response;
pub use id::ProviderId;
pub use request::{ProviderRequest, ProviderRequestError, ProviderRequestType};
pub use response::{ProviderResponse, ProviderResponseType, ProviderStreamResponse, TokenUsage};
pub use response::{ProviderResponse, ProviderResponseType, TokenUsage};
pub use streaming_response::{ProviderStreamResponse, ProviderStreamResponseType};

View file

@ -2,19 +2,21 @@ use crate::apis::anthropic::MessagesRequest;
use crate::apis::openai::ChatCompletionsRequest;
use crate::apis::amazon_bedrock::{ConverseRequest, ConverseStreamRequest};
use crate::clients::endpoints::SupportedAPIs;
use crate::apis::openai_responses::ResponsesAPIRequest;
use crate::clients::endpoints::SupportedAPIsFromClient;
use crate::clients::endpoints::SupportedUpstreamAPIs;
use serde_json::Value;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum ProviderRequestType {
ChatCompletionsRequest(ChatCompletionsRequest),
MessagesRequest(MessagesRequest),
BedrockConverse(ConverseRequest),
BedrockConverseStream(ConverseStreamRequest),
ResponsesAPIRequest(ResponsesAPIRequest),
//add more request types here
}
pub trait ProviderRequest: Send + Sync {
@ -49,6 +51,7 @@ impl ProviderRequest for ProviderRequestType {
Self::MessagesRequest(r) => r.model(),
Self::BedrockConverse(r) => r.model(),
Self::BedrockConverseStream(r) => r.model(),
Self::ResponsesAPIRequest(r) => r.model(),
}
}
@ -58,6 +61,7 @@ impl ProviderRequest for ProviderRequestType {
Self::MessagesRequest(r) => r.set_model(model),
Self::BedrockConverse(r) => r.set_model(model),
Self::BedrockConverseStream(r) => r.set_model(model),
Self::ResponsesAPIRequest(r) => r.set_model(model),
}
}
@ -67,6 +71,7 @@ impl ProviderRequest for ProviderRequestType {
Self::MessagesRequest(r) => r.is_streaming(),
Self::BedrockConverse(_) => false,
Self::BedrockConverseStream(_) => true,
Self::ResponsesAPIRequest(r) => r.is_streaming(),
}
}
@ -76,6 +81,7 @@ impl ProviderRequest for ProviderRequestType {
Self::MessagesRequest(r) => r.extract_messages_text(),
Self::BedrockConverse(r) => r.extract_messages_text(),
Self::BedrockConverseStream(r) => r.extract_messages_text(),
Self::ResponsesAPIRequest(r) => r.extract_messages_text(),
}
}
@ -85,6 +91,7 @@ impl ProviderRequest for ProviderRequestType {
Self::MessagesRequest(r) => r.get_recent_user_message(),
Self::BedrockConverse(r) => r.get_recent_user_message(),
Self::BedrockConverseStream(r) => r.get_recent_user_message(),
Self::ResponsesAPIRequest(r) => r.get_recent_user_message(),
}
}
@ -94,6 +101,7 @@ impl ProviderRequest for ProviderRequestType {
Self::MessagesRequest(r) => r.to_bytes(),
Self::BedrockConverse(r) => r.to_bytes(),
Self::BedrockConverseStream(r) => r.to_bytes(),
Self::ResponsesAPIRequest(r) => r.to_bytes(),
}
}
@ -103,6 +111,7 @@ impl ProviderRequest for ProviderRequestType {
Self::MessagesRequest(r) => r.metadata(),
Self::BedrockConverse(r) => r.metadata(),
Self::BedrockConverseStream(r) => r.metadata(),
Self::ResponsesAPIRequest(r) => r.metadata(),
}
}
@ -112,18 +121,19 @@ impl ProviderRequest for ProviderRequestType {
Self::MessagesRequest(r) => r.remove_metadata_key(key),
Self::BedrockConverse(r) => r.remove_metadata_key(key),
Self::BedrockConverseStream(r) => r.remove_metadata_key(key),
Self::ResponsesAPIRequest(r) => r.remove_metadata_key(key),
}
}
}
/// Parse the client API from a byte slice.
impl TryFrom<(&[u8], &SupportedAPIs)> for ProviderRequestType {
impl TryFrom<(&[u8], &SupportedAPIsFromClient)> for ProviderRequestType {
type Error = std::io::Error;
fn try_from((bytes, client_api): (&[u8], &SupportedAPIs)) -> Result<Self, Self::Error> {
fn try_from((bytes, client_api): (&[u8], &SupportedAPIsFromClient)) -> Result<Self, Self::Error> {
// Use SupportedApi to determine the appropriate request type
match client_api {
SupportedAPIs::OpenAIChatCompletions(_) => {
SupportedAPIsFromClient::OpenAIChatCompletions(_) => {
let chat_completion_request: ChatCompletionsRequest =
ChatCompletionsRequest::try_from(bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
@ -131,11 +141,20 @@ impl TryFrom<(&[u8], &SupportedAPIs)> for ProviderRequestType {
chat_completion_request,
))
}
SupportedAPIs::AnthropicMessagesAPI(_) => {
SupportedAPIsFromClient::AnthropicMessagesAPI(_) => {
let messages_request: MessagesRequest = MessagesRequest::try_from(bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(ProviderRequestType::MessagesRequest(messages_request))
}
SupportedAPIsFromClient::OpenAIResponsesAPI(_) => {
let responses_apirequest: ResponsesAPIRequest =
ResponsesAPIRequest::try_from(bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(ProviderRequestType::ResponsesAPIRequest(
responses_apirequest,
))
}
}
}
}
@ -148,17 +167,13 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT
(client_request, upstream_api): (ProviderRequestType, &SupportedUpstreamAPIs),
) -> Result<Self, Self::Error> {
match (client_request, upstream_api) {
// Same API - no conversion needed, just clone the reference
// ============================================================================
// ChatCompletionsRequest conversions
// ============================================================================
(
ProviderRequestType::ChatCompletionsRequest(chat_req),
SupportedUpstreamAPIs::OpenAIChatCompletions(_),
) => Ok(ProviderRequestType::ChatCompletionsRequest(chat_req)),
(
ProviderRequestType::MessagesRequest(messages_req),
SupportedUpstreamAPIs::AnthropicMessagesAPI(_),
) => Ok(ProviderRequestType::MessagesRequest(messages_req)),
// Cross-API conversion - cloning is necessary for transformation
(
ProviderRequestType::ChatCompletionsRequest(chat_req),
SupportedUpstreamAPIs::AnthropicMessagesAPI(_),
@ -173,7 +188,45 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT
})?;
Ok(ProviderRequestType::MessagesRequest(messages_req))
}
(
ProviderRequestType::ChatCompletionsRequest(chat_req),
SupportedUpstreamAPIs::AmazonBedrockConverse(_),
) => {
let bedrock_req = ConverseRequest::try_from(chat_req)
.map_err(|e| ProviderRequestError {
message: format!("Failed to convert ChatCompletionsRequest to Amazon Bedrock request: {}", e),
source: Some(Box::new(e))
})?;
Ok(ProviderRequestType::BedrockConverse(bedrock_req))
}
(
ProviderRequestType::ChatCompletionsRequest(chat_req),
SupportedUpstreamAPIs::AmazonBedrockConverseStream(_),
) => {
let bedrock_req = ConverseStreamRequest::try_from(chat_req)
.map_err(|e| ProviderRequestError {
message: format!("Failed to convert ChatCompletionsRequest to Amazon Bedrock Stream request: {}", e),
source: Some(Box::new(e))
})?;
Ok(ProviderRequestType::BedrockConverseStream(bedrock_req))
}
(
ProviderRequestType::ChatCompletionsRequest(_),
SupportedUpstreamAPIs::OpenAIResponsesAPI(_),
) => {
Err(ProviderRequestError {
message: "Conversion from ChatCompletionsRequest to ResponsesAPIRequest is not supported. ResponsesAPI can only be used as a client API, not as an upstream API.".to_string(),
source: None,
})
}
// ============================================================================
// MessagesRequest conversions
// ============================================================================
(
ProviderRequestType::MessagesRequest(messages_req),
SupportedUpstreamAPIs::AnthropicMessagesAPI(_),
) => Ok(ProviderRequestType::MessagesRequest(messages_req)),
(
ProviderRequestType::MessagesRequest(messages_req),
SupportedUpstreamAPIs::OpenAIChatCompletions(_),
@ -189,31 +242,6 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT
})?;
Ok(ProviderRequestType::ChatCompletionsRequest(chat_req))
}
// Cross-API conversions: OpenAI/Anthropic to Amazon Bedrock
(
ProviderRequestType::ChatCompletionsRequest(chat_req),
SupportedUpstreamAPIs::AmazonBedrockConverse(_),
) => {
let bedrock_req = ConverseRequest::try_from(chat_req)
.map_err(|e| ProviderRequestError {
message: format!("Failed to convert ChatCompletionsRequest to Amazon Bedrock request: {}", e),
source: Some(Box::new(e))
})?;
Ok(ProviderRequestType::BedrockConverse(bedrock_req))
}
(
ProviderRequestType::ChatCompletionsRequest(chat_req),
SupportedUpstreamAPIs::AmazonBedrockConverseStream(_),
) => {
let bedrock_req = ConverseStreamRequest::try_from(chat_req)
.map_err(|e| ProviderRequestError {
message: format!("Failed to convert ChatCompletionsRequest to Amazon Bedrock request: {}", e),
source: Some(Box::new(e))
})?;
Ok(ProviderRequestType::BedrockConverse(bedrock_req))
}
(
ProviderRequestType::MessagesRequest(messages_req),
SupportedUpstreamAPIs::AmazonBedrockConverse(_),
@ -235,7 +263,97 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT
let bedrock_req = ConverseStreamRequest::try_from(messages_req).map_err(|e| {
ProviderRequestError {
message: format!(
"Failed to convert MessagesRequest to Amazon Bedrock request: {}",
"Failed to convert MessagesRequest to Amazon Bedrock Stream request: {}",
e
),
source: Some(Box::new(e)),
}
})?;
Ok(ProviderRequestType::BedrockConverseStream(bedrock_req))
}
(
ProviderRequestType::MessagesRequest(_),
SupportedUpstreamAPIs::OpenAIResponsesAPI(_),
) => {
Err(ProviderRequestError {
message: "Conversion from MessagesRequest to ResponsesAPIRequest is not supported. ResponsesAPI can only be used as a client API, not as an upstream API.".to_string(),
source: None,
})
}
// ============================================================================
// ResponsesAPIRequest conversions (only converts TO other formats)
// ============================================================================
(
ProviderRequestType::ResponsesAPIRequest(responses_req),
SupportedUpstreamAPIs::OpenAIResponsesAPI(_),
) => Ok(ProviderRequestType::ResponsesAPIRequest(responses_req)),
// ResponsesAPI -> ChatCompletions (direct conversion)
(
ProviderRequestType::ResponsesAPIRequest(responses_req),
SupportedUpstreamAPIs::OpenAIChatCompletions(_),
) => {
let chat_req = ChatCompletionsRequest::try_from(responses_req).map_err(|e| {
ProviderRequestError {
message: format!(
"Failed to convert ResponsesAPIRequest to ChatCompletionsRequest: {}",
e
),
source: Some(Box::new(e)),
}
})?;
Ok(ProviderRequestType::ChatCompletionsRequest(chat_req))
}
// ResponsesAPI -> Anthropic Messages (via ChatCompletions)
(
ProviderRequestType::ResponsesAPIRequest(responses_req),
SupportedUpstreamAPIs::AnthropicMessagesAPI(_),
) => {
// Chain: ResponsesAPI -> ChatCompletions -> MessagesRequest
let chat_req = ChatCompletionsRequest::try_from(responses_req).map_err(|e| {
ProviderRequestError {
message: format!(
"Failed to convert ResponsesAPIRequest to ChatCompletionsRequest: {}",
e
),
source: Some(Box::new(e)),
}
})?;
let messages_req = MessagesRequest::try_from(chat_req).map_err(|e| {
ProviderRequestError {
message: format!(
"Failed to convert ChatCompletionsRequest to MessagesRequest: {}",
e
),
source: Some(Box::new(e)),
}
})?;
Ok(ProviderRequestType::MessagesRequest(messages_req))
}
// ResponsesAPI -> Bedrock Converse (via ChatCompletions)
(
ProviderRequestType::ResponsesAPIRequest(responses_req),
SupportedUpstreamAPIs::AmazonBedrockConverse(_),
) => {
// Chain: ResponsesAPI -> ChatCompletions -> ConverseRequest
let chat_req = ChatCompletionsRequest::try_from(responses_req).map_err(|e| {
ProviderRequestError {
message: format!(
"Failed to convert ResponsesAPIRequest to ChatCompletionsRequest: {}",
e
),
source: Some(Box::new(e)),
}
})?;
let bedrock_req = ConverseRequest::try_from(chat_req).map_err(|e| {
ProviderRequestError {
message: format!(
"Failed to convert ChatCompletionsRequest to Amazon Bedrock request: {}",
e
),
source: Some(Box::new(e)),
@ -244,13 +362,50 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT
Ok(ProviderRequestType::BedrockConverse(bedrock_req))
}
// Amazon Bedrock to other APIs conversions
// ResponsesAPI -> Bedrock Converse Stream (via ChatCompletions)
(
ProviderRequestType::ResponsesAPIRequest(responses_req),
SupportedUpstreamAPIs::AmazonBedrockConverseStream(_),
) => {
// Chain: ResponsesAPI -> ChatCompletions -> ConverseStreamRequest
let chat_req = ChatCompletionsRequest::try_from(responses_req).map_err(|e| {
ProviderRequestError {
message: format!(
"Failed to convert ResponsesAPIRequest to ChatCompletionsRequest: {}",
e
),
source: Some(Box::new(e)),
}
})?;
let bedrock_req = ConverseStreamRequest::try_from(chat_req).map_err(|e| {
ProviderRequestError {
message: format!(
"Failed to convert ChatCompletionsRequest to Amazon Bedrock Stream request: {}",
e
),
source: Some(Box::new(e)),
}
})?;
Ok(ProviderRequestType::BedrockConverseStream(bedrock_req))
}
// ============================================================================
// Amazon Bedrock conversions (not supported as client API)
// ============================================================================
(ProviderRequestType::BedrockConverse(_), _) => {
todo!("Amazon Bedrock to ChatCompletionsRequest conversion not implemented yet")
Err(ProviderRequestError {
message: "Amazon Bedrock Converse is not supported as a client API. Only OpenAI ChatCompletions, Anthropic Messages, and OpenAI Responses APIs are supported as client APIs.".to_string(),
source: None,
})
}
(ProviderRequestType::BedrockConverseStream(_), _) => {
todo!("Amazon Bedrock Stream to ChatCompletionsRequest conversion not implemented yet")
Err(ProviderRequestError {
message: "Amazon Bedrock Converse Stream is not supported as a client API. Only OpenAI ChatCompletions, Anthropic Messages, and OpenAI Responses APIs are supported as client APIs.".to_string(),
source: None,
})
}
}
}
@ -284,7 +439,7 @@ mod tests {
use crate::apis::anthropic::MessagesRequest as AnthropicMessagesRequest;
use crate::apis::openai::ChatCompletionsRequest;
use crate::apis::openai::OpenAIApi::ChatCompletions;
use crate::clients::endpoints::SupportedAPIs;
use crate::clients::endpoints::SupportedAPIsFromClient;
use crate::transforms::lib::ExtractText;
use serde_json::json;
@ -298,7 +453,7 @@ mod tests {
]
});
let bytes = serde_json::to_vec(&req).unwrap();
let api = SupportedAPIs::OpenAIChatCompletions(ChatCompletions);
let api = SupportedAPIsFromClient::OpenAIChatCompletions(ChatCompletions);
let result = ProviderRequestType::try_from((bytes.as_slice(), &api));
assert!(result.is_ok());
match result.unwrap() {
@ -321,7 +476,7 @@ mod tests {
]
});
let bytes = serde_json::to_vec(&req).unwrap();
let endpoint = SupportedAPIs::AnthropicMessagesAPI(Messages);
let endpoint = SupportedAPIsFromClient::AnthropicMessagesAPI(Messages);
let result = ProviderRequestType::try_from((bytes.as_slice(), &endpoint));
assert!(result.is_ok());
match result.unwrap() {
@ -343,7 +498,7 @@ mod tests {
]
});
let bytes = serde_json::to_vec(&req).unwrap();
let endpoint = SupportedAPIs::OpenAIChatCompletions(ChatCompletions);
let endpoint = SupportedAPIsFromClient::OpenAIChatCompletions(ChatCompletions);
let result = ProviderRequestType::try_from((bytes.as_slice(), &endpoint));
assert!(result.is_ok());
match result.unwrap() {
@ -366,7 +521,7 @@ mod tests {
});
let bytes = serde_json::to_vec(&req).unwrap();
// Intentionally use OpenAI endpoint for Anthropic payload
let endpoint = SupportedAPIs::OpenAIChatCompletions(ChatCompletions);
let endpoint = SupportedAPIsFromClient::OpenAIChatCompletions(ChatCompletions);
let result = ProviderRequestType::try_from((bytes.as_slice(), &endpoint));
// Should parse as ChatCompletionsRequest, not error
assert!(result.is_ok());
@ -486,4 +641,272 @@ mod tests {
let roundtrip_max_tokens = openai_req2.max_completion_tokens.or(openai_req2.max_tokens);
assert_eq!(original_max_tokens, roundtrip_max_tokens);
}
#[test]
fn test_responses_api_request_from_bytes() {
use crate::apis::openai::OpenAIApi::Responses;
let req = json!({
"model": "gpt-4o",
"input": "Hello, how are you?"
});
let bytes = serde_json::to_vec(&req).unwrap();
let api = SupportedAPIsFromClient::OpenAIResponsesAPI(Responses);
let result = ProviderRequestType::try_from((bytes.as_slice(), &api));
assert!(result.is_ok());
match result.unwrap() {
ProviderRequestType::ResponsesAPIRequest(r) => {
assert_eq!(r.model, "gpt-4o");
}
_ => panic!("Expected ResponsesAPIRequest variant"),
}
}
#[test]
fn test_responses_api_to_chat_completions_conversion() {
use crate::apis::openai::OpenAIApi::ChatCompletions;
use crate::apis::openai_responses::{InputParam, ResponsesAPIRequest};
let responses_req = ResponsesAPIRequest {
model: "gpt-4o".to_string(),
input: InputParam::Text("Hello, world!".to_string()),
temperature: Some(0.7),
top_p: Some(0.9),
max_output_tokens: Some(100),
stream: Some(false),
metadata: None,
tools: None,
tool_choice: None,
parallel_tool_calls: None,
instructions: None,
modalities: None,
user: None,
store: None,
reasoning_effort: None,
include: None,
audio: None,
text: None,
service_tier: None,
top_logprobs: None,
stream_options: None,
truncation: None,
conversation: None,
previous_response_id: None,
max_tool_calls: None,
background: None,
};
let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(ChatCompletions);
let result = ProviderRequestType::try_from((
ProviderRequestType::ResponsesAPIRequest(responses_req),
&upstream_api,
));
assert!(result.is_ok());
match result.unwrap() {
ProviderRequestType::ChatCompletionsRequest(chat_req) => {
assert_eq!(chat_req.model, "gpt-4o");
assert_eq!(chat_req.temperature, Some(0.7));
assert_eq!(chat_req.top_p, Some(0.9));
assert_eq!(chat_req.max_completion_tokens, Some(100));
assert_eq!(chat_req.messages.len(), 1);
}
_ => panic!("Expected ChatCompletionsRequest variant"),
}
}
#[test]
fn test_responses_api_to_anthropic_messages_conversion() {
use crate::apis::anthropic::AnthropicApi::Messages;
use crate::apis::openai_responses::{InputParam, ResponsesAPIRequest};
let responses_req = ResponsesAPIRequest {
model: "gpt-4o".to_string(),
input: InputParam::Text("Hello, Claude!".to_string()),
temperature: Some(0.8),
max_output_tokens: Some(150),
stream: Some(false),
metadata: None,
tools: None,
tool_choice: None,
parallel_tool_calls: None,
instructions: Some("You are a helpful assistant".to_string()),
modalities: None,
user: None,
store: None,
reasoning_effort: None,
include: None,
audio: None,
text: None,
service_tier: None,
top_p: None,
top_logprobs: None,
stream_options: None,
truncation: None,
conversation: None,
previous_response_id: None,
max_tool_calls: None,
background: None,
};
let upstream_api = SupportedUpstreamAPIs::AnthropicMessagesAPI(Messages);
let result = ProviderRequestType::try_from((
ProviderRequestType::ResponsesAPIRequest(responses_req),
&upstream_api,
));
assert!(result.is_ok());
match result.unwrap() {
ProviderRequestType::MessagesRequest(messages_req) => {
assert_eq!(messages_req.model, "gpt-4o");
assert_eq!(messages_req.temperature, Some(0.8));
assert_eq!(messages_req.max_tokens, 150);
// Instructions should be converted to system prompt via ChatCompletions conversion
// The conversion chain: ResponsesAPI -> ChatCompletions (system message) -> Anthropic (system prompt)
// But we need to check if the system prompt was actually set
assert_eq!(messages_req.messages.len(), 1);
}
_ => panic!("Expected MessagesRequest variant"),
}
}
#[test]
fn test_responses_api_to_bedrock_conversion() {
use crate::apis::amazon_bedrock::AmazonBedrockApi::Converse;
use crate::apis::openai_responses::{InputParam, ResponsesAPIRequest};
let responses_req = ResponsesAPIRequest {
model: "gpt-4o".to_string(),
input: InputParam::Text("Hello, Bedrock!".to_string()),
temperature: Some(0.5),
max_output_tokens: Some(200),
stream: Some(false),
metadata: None,
tools: None,
tool_choice: None,
parallel_tool_calls: None,
instructions: None,
modalities: None,
user: None,
store: None,
reasoning_effort: None,
include: None,
audio: None,
text: None,
service_tier: None,
top_p: None,
top_logprobs: None,
stream_options: None,
truncation: None,
conversation: None,
previous_response_id: None,
max_tool_calls: None,
background: None,
};
let upstream_api = SupportedUpstreamAPIs::AmazonBedrockConverse(Converse);
let result = ProviderRequestType::try_from((
ProviderRequestType::ResponsesAPIRequest(responses_req),
&upstream_api,
));
assert!(result.is_ok());
match result.unwrap() {
ProviderRequestType::BedrockConverse(bedrock_req) => {
assert_eq!(bedrock_req.model_id, "gpt-4o");
// Bedrock receives the converted request through ChatCompletions
assert!(!bedrock_req.messages.is_none());
}
_ => panic!("Expected BedrockConverse variant"),
}
}
#[test]
fn test_chat_completions_to_responses_api_not_supported() {
use crate::apis::openai::OpenAIApi::Responses;
use crate::apis::openai::{Message, MessageContent, Role};
let chat_req = ChatCompletionsRequest {
model: "gpt-4".to_string(),
messages: vec![Message {
role: Role::User,
content: MessageContent::Text("Hello!".to_string()),
name: None,
tool_calls: None,
tool_call_id: None,
}],
..Default::default()
};
let upstream_api = SupportedUpstreamAPIs::OpenAIResponsesAPI(Responses);
let result = ProviderRequestType::try_from((
ProviderRequestType::ChatCompletionsRequest(chat_req),
&upstream_api,
));
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message.contains("ResponsesAPI can only be used as a client API"));
}
#[test]
fn test_anthropic_messages_to_responses_api_not_supported() {
use crate::apis::anthropic::MessagesRequest as AnthropicMessagesRequest;
use crate::apis::openai::OpenAIApi::Responses;
let messages_req = AnthropicMessagesRequest {
model: "claude-3-sonnet".to_string(),
messages: vec![crate::apis::anthropic::MessagesMessage {
role: crate::apis::anthropic::MessagesRole::User,
content: crate::apis::anthropic::MessagesMessageContent::Single(
"Hello!".to_string(),
),
}],
max_tokens: 100,
container: None,
mcp_servers: None,
service_tier: None,
thinking: None,
temperature: None,
top_p: None,
top_k: None,
stream: None,
stop_sequences: None,
system: None,
tools: None,
tool_choice: None,
metadata: None,
};
let upstream_api = SupportedUpstreamAPIs::OpenAIResponsesAPI(Responses);
let result = ProviderRequestType::try_from((
ProviderRequestType::MessagesRequest(messages_req),
&upstream_api,
));
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message.contains("ResponsesAPI can only be used as a client API"));
}
#[test]
fn test_bedrock_as_client_api_not_supported() {
use crate::apis::openai::OpenAIApi::ChatCompletions;
// Create a simple Bedrock request (we'll use Default if available, or minimal construction)
let bedrock_req = ConverseRequest::default();
let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(ChatCompletions);
let result = ProviderRequestType::try_from((
ProviderRequestType::BedrockConverse(bedrock_req),
&upstream_api,
));
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message.contains("not supported as a client API"));
assert!(err
.message
.contains("OpenAI ChatCompletions, Anthropic Messages, and OpenAI Responses"));
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff