Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 91 additions & 1 deletion sgl-router/src/routers/header_utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use axum::{body::Body, extract::Request, http::HeaderMap};
use axum::{
body::Body,
extract::Request,
http::{HeaderMap, HeaderValue},
};

/// Copy request headers to a Vec of name-value string pairs
/// Used for forwarding headers to backend workers
Expand Down Expand Up @@ -92,3 +96,89 @@ pub fn apply_request_headers(

request_builder
}

/// API provider types for provider-specific header handling
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiProvider {
Anthropic,
Xai,
OpenAi,
Gemini,
Generic,
}

impl ApiProvider {
/// Detect provider type from URL
pub fn from_url(url: &str) -> Self {
if url.contains("anthropic") {
ApiProvider::Anthropic
} else if url.contains("x.ai") {
ApiProvider::Xai
} else if url.contains("openai.com") {
ApiProvider::OpenAi
} else if url.contains("googleapis.com") {
ApiProvider::Gemini
} else {
ApiProvider::Generic
}
}
}

/// Apply provider-specific headers to request
pub fn apply_provider_headers(
mut req: reqwest::RequestBuilder,
url: &str,
auth_header: Option<&HeaderValue>,
) -> reqwest::RequestBuilder {
let provider = ApiProvider::from_url(url);

match provider {
ApiProvider::Anthropic => {
// Anthropic requires x-api-key instead of Authorization
// Extract Bearer token and use as x-api-key
if let Some(auth) = auth_header {
if let Ok(auth_str) = auth.to_str() {
let api_key = auth_str.strip_prefix("Bearer ").unwrap_or(auth_str);
req = req
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01");
}
}
}
ApiProvider::Gemini | ApiProvider::Xai | ApiProvider::OpenAi | ApiProvider::Generic => {
// Standard OpenAI-compatible: use Authorization header as-is
if let Some(auth) = auth_header {
req = req.header("Authorization", auth);
}
}
}

req
}

/// Extract auth header with passthrough semantics.
///
/// Passthrough mode: User's Authorization header takes priority.
/// Fallback: Worker's API key is used only if user didn't provide auth.
///
/// This enables use cases where:
/// 1. Users send their own API keys (multi-tenant, BYOK)
/// 2. Router has a default key for users who don't provide one
pub fn extract_auth_header(
headers: Option<&HeaderMap>,
worker_api_key: &Option<String>,
) -> Option<HeaderValue> {
// Passthrough: Try user's auth header first
let user_auth = headers.and_then(|h| {
h.get("authorization")
.or_else(|| h.get("Authorization"))
.cloned()
});

// Return user's auth if provided, otherwise use worker's API key
user_auth.or_else(|| {
worker_api_key
.as_ref()
.and_then(|k| HeaderValue::from_str(&format!("Bearer {}", k)).ok())
})
}
4 changes: 0 additions & 4 deletions sgl-router/src/routers/openai/conversations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ use crate::{
protocols::responses::{generate_id, ResponseInput, ResponsesRequest},
};

// ============================================================================
// Persistence Operations (OpenAI-specific)
// ============================================================================

/// Persist conversation items to storage
///
/// This function:
Expand Down
2 changes: 1 addition & 1 deletion sgl-router/src/routers/openai/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ use super::{
provider::ProviderRegistry,
responses::{mask_tools_as_mcp, patch_streaming_response_json},
streaming::handle_streaming_response,
utils::{apply_provider_headers, extract_auth_header},
};
use crate::{
app_context::AppContext,
Expand All @@ -48,6 +47,7 @@ use crate::{
ResponsesGetParams, ResponsesRequest,
},
},
routers::header_utils::{apply_provider_headers, extract_auth_header},
};

pub struct OpenAIRouter {
Expand Down
96 changes: 0 additions & 96 deletions sgl-router/src/routers/openai/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

use std::collections::HashMap;

use axum::http::HeaderValue;

// ============================================================================
// SSE Event Type Constants
// ============================================================================
Expand Down Expand Up @@ -95,100 +93,6 @@ impl OutputIndexMapper {
}
}

// ============================================================================
// Provider Detection and Header Handling
// ============================================================================

/// API provider types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiProvider {
Anthropic,
Xai,
OpenAi,
Gemini,
Generic,
}

impl ApiProvider {
/// Detect provider type from URL
pub fn from_url(url: &str) -> Self {
if url.contains("anthropic") {
ApiProvider::Anthropic
} else if url.contains("x.ai") {
ApiProvider::Xai
} else if url.contains("openai.com") {
ApiProvider::OpenAi
} else if url.contains("googleapis.com") {
ApiProvider::Gemini
} else {
ApiProvider::Generic
}
}
}

/// Apply provider-specific headers to request
pub fn apply_provider_headers(
mut req: reqwest::RequestBuilder,
url: &str,
auth_header: Option<&HeaderValue>,
) -> reqwest::RequestBuilder {
let provider = ApiProvider::from_url(url);

match provider {
ApiProvider::Anthropic => {
// Anthropic requires x-api-key instead of Authorization
// Extract Bearer token and use as x-api-key
if let Some(auth) = auth_header {
if let Ok(auth_str) = auth.to_str() {
let api_key = auth_str.strip_prefix("Bearer ").unwrap_or(auth_str);
req = req
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01");
}
}
}
ApiProvider::Gemini | ApiProvider::Xai | ApiProvider::OpenAi | ApiProvider::Generic => {
// Standard OpenAI-compatible: use Authorization header as-is
if let Some(auth) = auth_header {
req = req.header("Authorization", auth);
}
}
}

req
}

// ============================================================================
// Auth Header Resolution
// ============================================================================

/// Extract auth header with passthrough semantics.
///
/// Passthrough mode: User's Authorization header takes priority.
/// Fallback: Worker's API key is used only if user didn't provide auth.
///
/// This enables use cases where:
/// 1. Users send their own API keys (multi-tenant, BYOK)
/// 2. Router has a default key for users who don't provide one
pub fn extract_auth_header(
headers: Option<&http::HeaderMap>,
worker_api_key: &Option<String>,
) -> Option<HeaderValue> {
// Passthrough: Try user's auth header first
let user_auth = headers.and_then(|h| {
h.get("authorization")
.or_else(|| h.get("Authorization"))
.cloned()
});

// Return user's auth if provided, otherwise use worker's API key
user_auth.or_else(|| {
worker_api_key
.as_ref()
.and_then(|k| HeaderValue::from_str(&format!("Bearer {}", k)).ok())
})
}

// ============================================================================
// Re-export FunctionCallInProgress from mcp module
// ============================================================================
Expand Down
Loading