use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use serde_json::{Value, json};
use crate::anthropic_stream::map_stop_reason;
use crate::app_state::AppState;
use crate::bridge_selection::{ModelSelectionRequired, SelectionFailure};
use crate::config::UpstreamProvider;
use crate::metrics::Surface;
pub(crate) const DEFAULT_MAX_TOKENS: u64 = 4096;
#[must_use]
pub const fn is_bridged(provider: UpstreamProvider) -> bool {
matches!(
provider,
UpstreamProvider::Codex
| UpstreamProvider::Qwen
| UpstreamProvider::Gemini
| UpstreamProvider::OpenAICompatible
| UpstreamProvider::ZaiCodingPlan
)
}
pub fn resolve_bridge_model(state: &AppState) -> Result<String, ModelSelectionRequired> {
resolve_bridge_model_for_account(state, None)
}
fn resolve_bridge_model_for_account(
state: &AppState,
router_account: Option<&str>,
) -> Result<String, ModelSelectionRequired> {
let Some(provider) = state.upstream_provider.subscription_provider() else {
return Ok(state.bridge_model.clone().unwrap_or_else(|| {
state
.openai_compatible
.default_model
.clone()
.unwrap_or_default()
}));
};
let status = router_account.map_or_else(
|| state.model_catalogs.status(provider),
|account| state.model_catalogs.status_for(provider, account),
);
let fail = |reason| {
Err(ModelSelectionRequired {
provider: provider.as_str().to_string(),
reason,
})
};
if !status.discovered {
return fail(SelectionFailure::NotDiscovered);
}
if !status.credential_healthy {
return fail(SelectionFailure::CredentialUnavailable);
}
if let Some(model) = state
.bridge_model
.as_deref()
.filter(|model| !model.is_empty())
{
return catalog_contains_current_generation(&status, model)
.then(|| model.to_string())
.map_or_else(|| fail(SelectionFailure::ConfiguredModelUnavailable), Ok);
}
let selected = state
.bridge_model_policy
.choose(status.routable_models())
.map_or_else(|| fail(SelectionFailure::EmptyCatalog), Ok)?;
if catalog_contains_current_generation(&status, &selected) {
Ok(selected)
} else {
fail(SelectionFailure::CredentialUnavailable)
}
}
fn catalog_contains_current_generation(
status: &crate::model_catalog::CatalogStatus,
model: &str,
) -> bool {
let expected_account = status.account.as_deref();
let Some(record) = status
.records
.iter()
.find(|record| record.canonical_id == model)
else {
return false;
};
(!record.health_generation.is_empty())
&& expected_account.is_none_or(|account| record.account == account)
&& status.records.iter().all(|candidate| {
candidate.health_generation == record.health_generation
&& expected_account.is_none_or(|account| candidate.account == account)
})
}
pub use crate::bridge_request::anthropic_to_chat_request;
#[must_use]
pub fn openai_json_to_anthropic_message(payload: &Value, requested_model: &str) -> Value {
try_openai_json_to_anthropic_message(payload, requested_model).unwrap_or_else(|message| {
json!({
"type": "error",
"error": {"type": "api_error", "message": message},
})
})
}
pub fn try_openai_json_to_anthropic_message(
payload: &Value,
requested_model: &str,
) -> Result<Value, String> {
if payload.get("object").and_then(Value::as_str) == Some("response")
|| payload.get("output").is_some()
{
responses_to_anthropic_message(payload, requested_model)
} else {
chat_completion_to_anthropic_message(payload, requested_model)
}
}
fn chat_completion_to_anthropic_message(
payload: &Value,
requested_model: &str,
) -> Result<Value, String> {
let choice = payload
.get("choices")
.and_then(Value::as_array)
.and_then(|c| c.first())
.cloned()
.unwrap_or(Value::Null);
let message = choice.get("message").unwrap_or(&Value::Null);
let mut content: Vec<Value> = Vec::new();
if let Some(text) = message.get("content").and_then(Value::as_str)
&& !text.is_empty()
{
let citations = crate::bridge_response::openai_annotations_to_anthropic(
text,
message.get("annotations"),
true,
)?;
let mut block = json!({"type": "text", "text": text});
if !citations.is_empty() {
block["citations"] = Value::Array(citations);
}
content.push(block);
}
for call in message
.get("tool_calls")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default()
{
content.push(tool_use_block(
call.get("id").and_then(Value::as_str).unwrap_or_default(),
call.get("function")
.and_then(|f| f.get("name"))
.and_then(Value::as_str)
.unwrap_or_default(),
call.get("function")
.and_then(|f| f.get("arguments"))
.and_then(Value::as_str)
.unwrap_or("{}"),
)?);
}
let stop_reason = choice
.get("finish_reason")
.and_then(Value::as_str)
.map_or("end_turn", map_stop_reason);
let usage = payload.get("usage");
let mut translated = message_envelope(
payload.get("id").and_then(Value::as_str),
requested_model,
&content,
stop_reason,
usage_field(usage, &["prompt_tokens", "input_tokens"]),
usage_field(usage, &["completion_tokens", "output_tokens"]),
);
translated["usage"] = crate::bridge_response::openai_usage_to_anthropic(usage);
if let Some(tier) =
crate::bridge_response::anthropic_service_tier_from_openai(payload.get("service_tier"))
{
translated["usage"]["service_tier"] = Value::String(tier.into());
}
Ok(translated)
}
fn responses_to_anthropic_message(payload: &Value, requested_model: &str) -> Result<Value, String> {
let mut content: Vec<Value> = Vec::new();
let mut saw_tool_call = false;
let mut web_search_requests = 0_u64;
let output = payload
.get("output")
.and_then(Value::as_array)
.ok_or_else(|| "Responses output must be an array".to_string())?;
for item in output {
let kind = item
.get("type")
.and_then(Value::as_str)
.filter(|kind| !kind.is_empty())
.ok_or_else(|| "Responses output item type must be a non-empty string".to_string())?;
match kind {
"message" => {
let mut combined_text = String::new();
let mut combined_citations = Vec::new();
let parts = item
.get("content")
.and_then(Value::as_array)
.ok_or_else(|| "Responses message content must be an array".to_string())?;
for part in parts {
let part_kind = part
.get("type")
.and_then(Value::as_str)
.filter(|kind| !kind.is_empty())
.ok_or_else(|| {
"Responses message content type must be a non-empty string".to_string()
})?;
let text = match part_kind {
"output_text" | "text" => part.get("text").and_then(Value::as_str),
"refusal" => part.get("refusal").and_then(Value::as_str),
other => {
return Err(format!(
"Responses message content type {other} cannot be represented by Anthropic"
));
}
};
let Some(text) = text.filter(|text| !text.is_empty()) else {
continue;
};
let citations = crate::bridge_response::openai_annotations_to_anthropic(
text,
part.get("annotations"),
false,
)?;
combined_text.push_str(text);
combined_citations.extend(citations);
}
if !combined_text.is_empty() {
let mut block = json!({"type": "text", "text": combined_text});
if !combined_citations.is_empty() {
block["citations"] = Value::Array(combined_citations);
}
content.push(block);
}
}
"function_call" => {
saw_tool_call = true;
content.push(tool_use_block(
item.get("call_id")
.or_else(|| item.get("id"))
.and_then(Value::as_str)
.unwrap_or_default(),
item.get("name").and_then(Value::as_str).unwrap_or_default(),
item.get("arguments")
.and_then(Value::as_str)
.unwrap_or("{}"),
)?);
}
"web_search_call" => {
let id = item.get("id").and_then(Value::as_str).unwrap_or_default();
content.push(json!({
"type": "server_tool_use",
"id": id,
"name": "web_search",
"input": item.get("action").cloned().unwrap_or_else(|| json!({})),
}));
if item.get("status").and_then(Value::as_str) == Some("completed") {
web_search_requests = web_search_requests.saturating_add(1);
content.push(json!({
"type": "web_search_tool_result",
"tool_use_id": id,
"content": [],
}));
}
}
other => return Err(unrepresentable_responses_output(other)),
}
}
let stop_reason = if saw_tool_call {
"tool_use"
} else if payload.get("status").and_then(Value::as_str) == Some("incomplete") {
"max_tokens"
} else {
"end_turn"
};
let usage = payload.get("usage");
let mut message = message_envelope(
payload.get("id").and_then(Value::as_str),
requested_model,
&content,
stop_reason,
usage_field(usage, &["input_tokens", "prompt_tokens"]),
usage_field(usage, &["output_tokens", "completion_tokens"]),
);
message["usage"] = crate::bridge_response::openai_usage_to_anthropic(usage);
if let Some(tier) =
crate::bridge_response::anthropic_service_tier_from_openai(payload.get("service_tier"))
{
message["usage"]["service_tier"] = Value::String(tier.into());
}
if web_search_requests > 0 {
message["usage"]["server_tool_use"] = json!({
"web_search_requests": web_search_requests,
"web_fetch_requests": 0,
});
}
Ok(message)
}
pub(crate) fn unrepresentable_responses_output(kind: &str) -> String {
format!("Responses output item type {kind} cannot be represented by Anthropic")
}
fn tool_use_block(id: &str, name: &str, arguments: &str) -> Result<Value, String> {
let input = serde_json::from_str::<Value>(arguments)
.map_err(|_| "upstream function-call arguments must be valid JSON".to_string())?;
Ok(json!({
"type": "tool_use",
"id": if id.is_empty() { format!("toolu_{}", uuid::Uuid::new_v4().simple()) } else { id.to_string() },
"name": name,
"input": input,
}))
}
fn usage_field(usage: Option<&Value>, keys: &[&str]) -> u64 {
usage
.and_then(|u| keys.iter().find_map(|k| u.get(*k).and_then(Value::as_u64)))
.unwrap_or(0)
}
fn message_envelope(
id: Option<&str>,
model: &str,
content: &[Value],
stop_reason: &str,
input_tokens: u64,
output_tokens: u64,
) -> Value {
json!({
"id": id.map_or_else(|| format!("msg_{}", uuid::Uuid::new_v4().simple()), String::from),
"type": "message",
"role": "assistant",
"model": model,
"content": content,
"stop_reason": stop_reason,
"stop_sequence": Value::Null,
"usage": {"input_tokens": input_tokens, "output_tokens": output_tokens},
})
}
fn enforce_anthropic_stop(message: &mut Value, sequences: &[String]) {
let Some(content) = message.get_mut("content").and_then(Value::as_array_mut) else {
return;
};
let mut matched = None;
let mut keep = content.len();
for (index, block) in content.iter_mut().enumerate() {
let Some(text) = block.get_mut("text") else {
continue;
};
let Some(mut visible) = text.as_str().map(str::to_string) else {
continue;
};
if let Some(sequence) = crate::stop_sequences::truncate(&mut visible, sequences) {
*text = Value::String(visible);
matched = Some(sequence);
keep = index + 1;
break;
}
}
content.truncate(keep);
if let Some(sequence) = matched {
message["stop_reason"] = Value::String("end_turn".into());
message["stop_sequence"] = Value::String(sequence);
}
}
fn unsupported_server_tool(body: &Value, provider: UpstreamProvider) -> Option<String> {
provider.subscription_provider().and_then(|subscription| {
crate::capabilities::unsupported_server_tool_type(subscription, body.get("tools"))
})
}
pub(crate) fn untranslatable_anthropic_tool(body: &Value) -> Option<String> {
if let Some(tools) = body.get("tools") {
let Some(tools) = tools.as_array() else {
return Some("tools must be an array".into());
};
for tool in tools {
let kind = tool.get("type").and_then(Value::as_str);
if kind.is_some_and(|kind| {
kind.starts_with("web_search_") || kind.starts_with("web_fetch_")
}) {
continue;
}
if let Some(kind) = kind
&& kind != "custom"
{
return Some(format!("unsupported Anthropic tool type: {kind}"));
}
if tool.get("name").and_then(Value::as_str).is_none() {
return Some("client tool is missing a string name".into());
}
if tool
.get("input_schema")
.is_some_and(|schema| !schema.is_object())
{
return Some("client tool input_schema must be an object".into());
}
if tool
.get("strict")
.is_some_and(|strict| !strict.is_boolean())
{
return Some("client tool strict must be a boolean".into());
}
}
}
if let Some(choice) = body.get("tool_choice") {
let Some(kind) = choice.get("type").and_then(Value::as_str) else {
return Some("tool_choice is missing a string type".into());
};
if !matches!(kind, "auto" | "any" | "none" | "tool") {
return Some(format!("unsupported Anthropic tool_choice type: {kind}"));
}
if kind == "tool" && choice.get("name").and_then(Value::as_str).is_none() {
return Some("tool_choice type=tool is missing a string name".into());
}
}
None
}
pub async fn handle_anthropic_surface(
state: &AppState,
headers: &HeaderMap,
path: &str,
body: Value,
) -> Response {
handle_anthropic_surface_routed(state, headers, path, body, None).await
}
pub(crate) async fn handle_anthropic_surface_routed(
state: &AppState,
headers: &HeaderMap,
path: &str,
body: Value,
subscription: Option<&crate::model_routing::ValidatedSubscription>,
) -> Response {
if state.upstream_provider == UpstreamProvider::ZaiCodingPlan {
if path.ends_with("/count_tokens") {
return crate::zai_coding_plan::count_tokens(state, headers, path, &body);
}
return crate::zai_coding_plan::forward(
state,
headers,
body,
path,
crate::client_policy::ClientProtocol::AnthropicMessages,
Surface::Anthropic,
)
.await;
}
if path.ends_with("/count_tokens") {
let claims = match count_tokens_claims(&state.token_manager, headers) {
Ok(claims) => claims,
Err(response) => return *response,
};
crate::audit::record_authorised_request(
state,
&claims,
Surface::Anthropic,
path,
Some(&body),
);
return anthropic_error(
StatusCode::SERVICE_UNAVAILABLE,
b"exact token counting is unavailable for the selected route",
);
}
forward_anthropic_messages_routed(state, headers, path, body, subscription).await
}
pub(crate) fn count_tokens_claims(
token_manager: &crate::token::TokenManager,
headers: &HeaderMap,
) -> Result<crate::token::TokenClaims, Box<Response>> {
let Some(token) = crate::proxy::extract_client_token(headers) else {
return Err(Box::new(anthropic_error(
StatusCode::UNAUTHORIZED,
crate::proxy::CREDENTIAL_CARRIER_HINT.as_bytes(),
)));
};
token_manager.validate_token(token).map_err(|e| {
let status = match &e {
crate::token::TokenError::Revoked => StatusCode::FORBIDDEN,
_ => StatusCode::UNAUTHORIZED,
};
Box::new(anthropic_error(status, e.client_message().as_bytes()))
})
}
pub async fn forward_anthropic_messages(
state: &AppState,
headers: &HeaderMap,
anthropic_body: Value,
) -> Response {
forward_anthropic_messages_routed(state, headers, "/v1/messages", anthropic_body, None).await
}
async fn forward_anthropic_messages_routed(
state: &AppState,
headers: &HeaderMap,
path: &str,
anthropic_body: Value,
subscription: Option<&crate::model_routing::ValidatedSubscription>,
) -> Response {
if anthropic_body
.get("max_tokens")
.and_then(Value::as_u64)
.is_none_or(|limit| limit == 0)
{
if let Err(response) = count_tokens_claims(&state.token_manager, headers) {
return *response;
}
return anthropic_error(StatusCode::BAD_REQUEST, b"max_tokens is required");
}
if anthropic_body
.get("messages")
.and_then(Value::as_array)
.is_none_or(Vec::is_empty)
{
if let Err(response) = count_tokens_claims(&state.token_manager, headers) {
return *response;
}
return anthropic_error(
StatusCode::BAD_REQUEST,
b"messages must contain at least one message",
);
}
if let Some(kind) = unsupported_server_tool(&anthropic_body, state.upstream_provider) {
if let Err(response) = count_tokens_claims(&state.token_manager, headers) {
return *response;
}
return anthropic_error(
StatusCode::BAD_REQUEST,
format!("Unsupported tool type for selected provider: {kind}").as_bytes(),
);
}
if let Some(reason) = crate::capabilities::unhonourable_server_tool_request(
anthropic_body.get("tools"),
anthropic_body.get("tool_choice"),
) {
if let Err(response) = count_tokens_claims(&state.token_manager, headers) {
return *response;
}
return anthropic_error(StatusCode::BAD_REQUEST, reason.as_bytes());
}
if let Some(reason) = untranslatable_anthropic_tool(&anthropic_body) {
if let Err(response) = count_tokens_claims(&state.token_manager, headers) {
return *response;
}
return anthropic_error(StatusCode::BAD_REQUEST, reason.as_bytes());
}
let bridge_target = match state.upstream_provider {
UpstreamProvider::Codex => crate::bridge_request::BridgeTarget::Responses,
UpstreamProvider::Gemini => crate::bridge_request::BridgeTarget::Gemini,
_ => crate::bridge_request::BridgeTarget::Chat,
};
if let Err(reason) =
crate::bridge_request::validate_anthropic_request(&anthropic_body, bridge_target)
{
if let Err(response) = count_tokens_claims(&state.token_manager, headers) {
return *response;
}
return anthropic_error(StatusCode::BAD_REQUEST, reason.as_bytes());
}
let requested_model = anthropic_body
.get("model")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let stream_requested = anthropic_body
.get("stream")
.and_then(Value::as_bool)
.unwrap_or(false);
let stop_sequences = crate::stop_sequences::from_value(anthropic_body.get("stop_sequences"));
let bound_subscription;
let subscription = if let Some(candidate) = subscription.filter(|item| item.uses_account_pool())
{
let claims = match count_tokens_claims(&state.token_manager, headers) {
Ok(claims) => claims,
Err(response) => return *response,
};
let pinned_account = match state.token_manager.account_for(&claims.sub) {
Ok(account) => account,
Err(error) => {
return crate::proxy::error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"api_error",
&format!("failed to resolve token account binding: {error}"),
);
}
};
let context = crate::request_routing::request_routing_context(
headers,
&anthropic_body,
pinned_account,
);
bound_subscription = match candidate.bind_for_context(state, &context).await {
Ok(subscription) => Some(subscription),
Err(error) => {
return crate::proxy::error_response(
StatusCode::SERVICE_UNAVAILABLE,
"account_unavailable",
&error,
);
}
};
bound_subscription.as_ref()
} else {
subscription
};
let upstream_model = match resolve_bridge_model_for_account(
state,
subscription.and_then(|item| item.account_name()),
) {
Ok(model) => model,
Err(error) => {
return crate::proxy::error_response(
StatusCode::SERVICE_UNAVAILABLE,
crate::bridge_selection::MODEL_SELECTION_REQUIRED,
&error.to_string(),
);
}
};
let upstream = match state.upstream_provider {
UpstreamProvider::Codex => {
let responses_body = match crate::bridge_request::anthropic_to_responses_request(
&anthropic_body,
&upstream_model,
) {
Ok(body) => body,
Err(reason) => {
return anthropic_error(StatusCode::BAD_REQUEST, reason.as_bytes());
}
};
let routing_body = responses_body.clone();
crate::subscription_proxy::forward_subscription_openai_routed(
state,
headers,
responses_body,
&routing_body,
"/v1/responses",
Surface::Anthropic,
crate::subscription_proxy::RoutedSubscriptionContext {
validated: subscription,
entitlement: None,
native_route: false,
},
)
.await
}
UpstreamProvider::Qwen => {
let chat_body = anthropic_to_chat_request(&anthropic_body, &upstream_model);
crate::subscription_proxy::forward_subscription_openai_routed(
state,
headers,
chat_body.clone(),
&chat_body,
"/v1/chat/completions",
Surface::Anthropic,
crate::subscription_proxy::RoutedSubscriptionContext {
validated: subscription,
entitlement: None,
native_route: false,
},
)
.await
}
UpstreamProvider::Gemini => {
let chat_body = anthropic_to_chat_request(&anthropic_body, &upstream_model);
crate::gemini::forward_chat_completions_as_routed(
state,
headers,
chat_body,
Surface::Anthropic,
subscription,
)
.await
}
_ => {
let chat_body = anthropic_to_chat_request(&anthropic_body, &upstream_model);
crate::provider_proxy::forward_provider_at_routed(
state,
headers,
chat_body.clone(),
&chat_body,
crate::provider_proxy::ProviderForwardOptions {
path,
upstream_path: "/v1/chat/completions",
surface: Surface::Anthropic,
copy_anthropic_headers: false,
protocol: crate::client_policy::ClientProtocol::AnthropicMessages,
native_protocol: false,
},
)
.await
}
};
translate_upstream_response(
upstream,
&requested_model,
&upstream_model,
stream_requested,
&stop_sequences,
)
.await
}
#[path = "anthropic_bridge_response.rs"]
mod response;
pub(crate) use response::translate_upstream_response;
#[path = "anthropic_bridge_error.rs"]
mod error;
use error::anthropic_error;