1
0
forked from boos/BoosAPI

feat: add workspace and claude messages support

This commit is contained in:
boos
2026-05-22 05:57:37 +08:00
parent c1074ddf71
commit 7cdb02ef1d
24 changed files with 3282 additions and 479 deletions
+191
View File
@@ -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(&current) {
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(&current) {
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(