feat: add workspace and claude messages support
This commit is contained in:
@@ -6,6 +6,13 @@
|
||||
// 3. Transforms the response back to OpenAI-compatible format
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::transformers::{
|
||||
anthropic::{
|
||||
format_messages_request_with_compat, parse_messages_response, parse_stream_event,
|
||||
AnthropicMessagesCompatibility,
|
||||
},
|
||||
canonical::CanonicalRequest,
|
||||
};
|
||||
use crate::AppState;
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
@@ -193,6 +200,190 @@ pub async fn execute_chat_completions_stream(
|
||||
execute_streaming_request(route, "/chat/completions", body).await
|
||||
}
|
||||
|
||||
/// Execute an Anthropic Messages API request against an Anthropic-shaped upstream.
|
||||
pub async fn execute_anthropic_messages(
|
||||
route: UpstreamRoute,
|
||||
request: CanonicalRequest,
|
||||
) -> AppResult<Value> {
|
||||
let client = http_client();
|
||||
let upstream_url = format!("{}/messages", route.base_url.trim_end_matches('/'));
|
||||
let compatibility = AnthropicMessagesCompatibility::default();
|
||||
let body = format_messages_request_with_compat(&request, &compatibility);
|
||||
let mut builder = client
|
||||
.post(&upstream_url)
|
||||
.header("x-api-key", &route.api_key)
|
||||
.header("Authorization", format!("Bearer {}", route.api_key))
|
||||
.header("Content-Type", "application/json");
|
||||
if let Some(version) = compatibility.anthropic_version.as_deref() {
|
||||
builder = builder.header("anthropic-version", version);
|
||||
}
|
||||
|
||||
let resp = builder
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AppError::UpstreamError(format!("Upstream request failed: {e}")))?;
|
||||
let status = resp.status();
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| AppError::UpstreamError(format!("Failed to read upstream response: {e}")))?;
|
||||
if !status.is_success() {
|
||||
return Err(AppError::UpstreamError(format!(
|
||||
"Upstream returned {status}: {text}"
|
||||
)));
|
||||
}
|
||||
let json = parse_upstream_json(&text, &upstream_url, status)?;
|
||||
let canonical = parse_messages_response(&json)
|
||||
.map_err(|err| AppError::UpstreamError(format!("Invalid Anthropic response: {err}")))?;
|
||||
Ok(canonical
|
||||
.metadata
|
||||
.unwrap_or_else(|| serde_json::json!({ "content": [] })))
|
||||
}
|
||||
|
||||
/// Execute a streaming Anthropic Messages request and return Anthropic SSE frames.
|
||||
pub async fn execute_anthropic_messages_stream(
|
||||
route: UpstreamRoute,
|
||||
request: CanonicalRequest,
|
||||
) -> AppResult<Vec<String>> {
|
||||
let client = http_client();
|
||||
let upstream_url = format!("{}/messages", route.base_url.trim_end_matches('/'));
|
||||
let compatibility = AnthropicMessagesCompatibility::default();
|
||||
let body = format_messages_request_with_compat(&request, &compatibility);
|
||||
let mut builder = client
|
||||
.post(&upstream_url)
|
||||
.header("x-api-key", &route.api_key)
|
||||
.header("Authorization", format!("Bearer {}", route.api_key))
|
||||
.header("Content-Type", "application/json");
|
||||
if let Some(version) = compatibility.anthropic_version.as_deref() {
|
||||
builder = builder.header("anthropic-version", version);
|
||||
}
|
||||
|
||||
let resp = builder
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AppError::UpstreamError(format!("Upstream request failed: {e}")))?;
|
||||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "unknown error".to_string());
|
||||
return Err(AppError::UpstreamError(format!(
|
||||
"Upstream returned {status}: {text}"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut frames = Vec::new();
|
||||
let mut stream = resp.bytes_stream();
|
||||
use futures::StreamExt;
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let bytes = chunk_result
|
||||
.map_err(|e| AppError::UpstreamError(format!("Stream error: {e}")))?;
|
||||
let text = String::from_utf8_lossy(&bytes).to_string();
|
||||
frames.extend(normalize_anthropic_sse_frames(&text));
|
||||
}
|
||||
if !frames
|
||||
.iter()
|
||||
.any(|frame| frame.starts_with("event: message_stop"))
|
||||
{
|
||||
frames.push("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n".to_string());
|
||||
}
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
fn normalize_anthropic_sse_frames(text: &str) -> Vec<String> {
|
||||
let mut frames = Vec::new();
|
||||
let mut current = String::new();
|
||||
for line in text.lines() {
|
||||
if line.trim().is_empty() {
|
||||
if !current.trim().is_empty() {
|
||||
if let Some(frame) = normalize_anthropic_sse_frame(¤t) {
|
||||
frames.push(frame);
|
||||
}
|
||||
current.clear();
|
||||
}
|
||||
} else {
|
||||
current.push_str(line);
|
||||
current.push('\n');
|
||||
}
|
||||
}
|
||||
if !current.trim().is_empty() {
|
||||
if let Some(frame) = normalize_anthropic_sse_frame(¤t) {
|
||||
frames.push(frame);
|
||||
}
|
||||
}
|
||||
frames
|
||||
}
|
||||
|
||||
fn normalize_anthropic_sse_frame(frame: &str) -> Option<String> {
|
||||
let event = parse_stream_event(frame)?;
|
||||
if event.choices.is_empty() {
|
||||
if frame.contains("message_stop") {
|
||||
return Some("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n".to_string());
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let choice = &event.choices[0];
|
||||
if choice.delta.role.is_some() {
|
||||
let id = event.id.unwrap_or_else(|| format!("msg_{}", uuid::Uuid::new_v4().simple()));
|
||||
return Some(format!(
|
||||
"event: message_start\ndata: {}\n\n",
|
||||
serde_json::json!({
|
||||
"type": "message_start",
|
||||
"message": {
|
||||
"id": id,
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [],
|
||||
"model": "",
|
||||
"stop_reason": null,
|
||||
"stop_sequence": null,
|
||||
"usage": event.usage,
|
||||
}
|
||||
})
|
||||
));
|
||||
}
|
||||
if let Some(content) = choice.delta.content.as_deref() {
|
||||
return Some(format!(
|
||||
"event: content_block_delta\ndata: {}\n\n",
|
||||
serde_json::json!({
|
||||
"type": "content_block_delta",
|
||||
"index": choice.index,
|
||||
"delta": {
|
||||
"type": "text_delta",
|
||||
"text": content,
|
||||
}
|
||||
})
|
||||
));
|
||||
}
|
||||
if let Some(finish_reason) = choice.finish_reason.as_deref() {
|
||||
return Some(format!(
|
||||
"event: message_delta\ndata: {}\n\n",
|
||||
serde_json::json!({
|
||||
"type": "message_delta",
|
||||
"delta": {
|
||||
"stop_reason": denormalize_anthropic_stop_reason(finish_reason),
|
||||
"stop_sequence": null,
|
||||
},
|
||||
"usage": event.usage,
|
||||
})
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn denormalize_anthropic_stop_reason(reason: &str) -> &str {
|
||||
match reason {
|
||||
"stop" => "end_turn",
|
||||
"length" => "max_tokens",
|
||||
"tool_calls" => "tool_use",
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a streaming request against any upstream path (e.g. /responses).
|
||||
/// Returns SSE `data: ...` lines verbatim.
|
||||
pub async fn execute_streaming_request(
|
||||
|
||||
Reference in New Issue
Block a user