mirror of
https://github.com/katanemo/plano.git
synced 2026-04-25 00:36:34 +02:00
fix: passthrough_auth accepts Anthropic x-api-key and normalizes to upstream format (#892)
Some checks are pending
CI / pre-commit (push) Waiting to run
CI / plano-tools-tests (push) Waiting to run
CI / native-smoke-test (push) Waiting to run
CI / docker-build (push) Waiting to run
CI / validate-config (push) Waiting to run
CI / security-scan (push) Blocked by required conditions
CI / test-prompt-gateway (push) Blocked by required conditions
CI / test-model-alias-routing (push) Blocked by required conditions
CI / test-responses-api-with-state (push) Blocked by required conditions
CI / e2e-plano-tests (3.10) (push) Blocked by required conditions
CI / e2e-plano-tests (3.11) (push) Blocked by required conditions
CI / e2e-plano-tests (3.12) (push) Blocked by required conditions
CI / e2e-plano-tests (3.13) (push) Blocked by required conditions
CI / e2e-plano-tests (3.14) (push) Blocked by required conditions
CI / e2e-demo-preference (push) Blocked by required conditions
CI / e2e-demo-currency (push) Blocked by required conditions
Publish docker image (latest) / build-arm64 (push) Waiting to run
Publish docker image (latest) / build-amd64 (push) Waiting to run
Publish docker image (latest) / create-manifest (push) Blocked by required conditions
Build and Deploy Documentation / build (push) Waiting to run
Some checks are pending
CI / pre-commit (push) Waiting to run
CI / plano-tools-tests (push) Waiting to run
CI / native-smoke-test (push) Waiting to run
CI / docker-build (push) Waiting to run
CI / validate-config (push) Waiting to run
CI / security-scan (push) Blocked by required conditions
CI / test-prompt-gateway (push) Blocked by required conditions
CI / test-model-alias-routing (push) Blocked by required conditions
CI / test-responses-api-with-state (push) Blocked by required conditions
CI / e2e-plano-tests (3.10) (push) Blocked by required conditions
CI / e2e-plano-tests (3.11) (push) Blocked by required conditions
CI / e2e-plano-tests (3.12) (push) Blocked by required conditions
CI / e2e-plano-tests (3.13) (push) Blocked by required conditions
CI / e2e-plano-tests (3.14) (push) Blocked by required conditions
CI / e2e-demo-preference (push) Blocked by required conditions
CI / e2e-demo-currency (push) Blocked by required conditions
Publish docker image (latest) / build-arm64 (push) Waiting to run
Publish docker image (latest) / build-amd64 (push) Waiting to run
Publish docker image (latest) / create-manifest (push) Blocked by required conditions
Build and Deploy Documentation / build (push) Waiting to run
This commit is contained in:
parent
0f67b2c806
commit
37600fd07a
1 changed files with 120 additions and 25 deletions
|
|
@ -177,24 +177,33 @@ impl StreamContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modify_auth_headers(&mut self) -> Result<(), ServerError> {
|
fn modify_auth_headers(&mut self) -> Result<(), ServerError> {
|
||||||
if self.llm_provider().passthrough_auth == Some(true) {
|
// Determine the credential to forward upstream. Either the client
|
||||||
// Check if client provided an Authorization header
|
// supplied one (passthrough_auth) or it's configured on the provider.
|
||||||
if self.get_http_request_header("Authorization").is_none() {
|
let credential: String = if self.llm_provider().passthrough_auth == Some(true) {
|
||||||
warn!(
|
// Client auth may arrive in either Anthropic-style (`x-api-key`)
|
||||||
"request_id={}: passthrough_auth enabled but no authorization header present in client request",
|
// or OpenAI-style (`Authorization: Bearer ...`). Accept both so
|
||||||
self.request_identifier()
|
// clients using Anthropic SDKs (which default to `x-api-key`)
|
||||||
);
|
// work when the upstream is OpenAI-compatible, and vice versa.
|
||||||
} else {
|
let authorization = self.get_http_request_header("Authorization");
|
||||||
|
let x_api_key = self.get_http_request_header("x-api-key");
|
||||||
|
match extract_client_credential(authorization.as_deref(), x_api_key.as_deref()) {
|
||||||
|
Some(key) => {
|
||||||
debug!(
|
debug!(
|
||||||
"request_id={}: preserving client authorization header for provider '{}'",
|
"request_id={}: forwarding client credential to provider '{}'",
|
||||||
self.request_identifier(),
|
self.request_identifier(),
|
||||||
self.llm_provider().name
|
self.llm_provider().name
|
||||||
);
|
);
|
||||||
|
key
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
warn!(
|
||||||
|
"request_id={}: passthrough_auth enabled but no Authorization / x-api-key header present in client request",
|
||||||
|
self.request_identifier()
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let llm_provider_api_key_value =
|
} else {
|
||||||
self.llm_provider()
|
self.llm_provider()
|
||||||
.access_key
|
.access_key
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -203,15 +212,19 @@ impl StreamContext {
|
||||||
"No access key configured for selected LLM Provider \"{}\"",
|
"No access key configured for selected LLM Provider \"{}\"",
|
||||||
self.llm_provider()
|
self.llm_provider()
|
||||||
),
|
),
|
||||||
})?;
|
})?
|
||||||
|
.clone()
|
||||||
|
};
|
||||||
|
|
||||||
// Set API-specific headers based on the resolved upstream API
|
// Normalize the credential into whichever header the upstream expects.
|
||||||
|
// This lets an Anthropic-SDK client reach an OpenAI-compatible upstream
|
||||||
|
// (and vice versa) without the caller needing to know what format the
|
||||||
|
// upstream uses.
|
||||||
match self.resolved_api.as_ref() {
|
match self.resolved_api.as_ref() {
|
||||||
Some(SupportedUpstreamAPIs::AnthropicMessagesAPI(_)) => {
|
Some(SupportedUpstreamAPIs::AnthropicMessagesAPI(_)) => {
|
||||||
// Anthropic API requires x-api-key and anthropic-version headers
|
// Anthropic expects `x-api-key` + `anthropic-version`.
|
||||||
// Remove any existing Authorization header since Anthropic doesn't use it
|
|
||||||
self.remove_http_request_header("Authorization");
|
self.remove_http_request_header("Authorization");
|
||||||
self.set_http_request_header("x-api-key", Some(llm_provider_api_key_value));
|
self.set_http_request_header("x-api-key", Some(&credential));
|
||||||
self.set_http_request_header("anthropic-version", Some("2023-06-01"));
|
self.set_http_request_header("anthropic-version", Some("2023-06-01"));
|
||||||
}
|
}
|
||||||
Some(
|
Some(
|
||||||
|
|
@ -221,10 +234,9 @@ impl StreamContext {
|
||||||
| SupportedUpstreamAPIs::OpenAIResponsesAPI(_),
|
| SupportedUpstreamAPIs::OpenAIResponsesAPI(_),
|
||||||
)
|
)
|
||||||
| None => {
|
| None => {
|
||||||
// OpenAI and default: use Authorization Bearer token
|
// OpenAI (and default): `Authorization: Bearer ...`.
|
||||||
// Remove any existing x-api-key header since OpenAI doesn't use it
|
|
||||||
self.remove_http_request_header("x-api-key");
|
self.remove_http_request_header("x-api-key");
|
||||||
let authorization_header_value = format!("Bearer {}", llm_provider_api_key_value);
|
let authorization_header_value = format!("Bearer {}", credential);
|
||||||
self.set_http_request_header("Authorization", Some(&authorization_header_value));
|
self.set_http_request_header("Authorization", Some(&authorization_header_value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1235,3 +1247,86 @@ fn current_time_ns() -> u128 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context for StreamContext {}
|
impl Context for StreamContext {}
|
||||||
|
|
||||||
|
/// Extract the credential a client sent in either an OpenAI-style
|
||||||
|
/// `Authorization` header or an Anthropic-style `x-api-key` header.
|
||||||
|
///
|
||||||
|
/// Returns `None` when neither header is present or both are empty/whitespace.
|
||||||
|
/// The `Bearer ` prefix on the `Authorization` value is stripped if present;
|
||||||
|
/// otherwise the value is taken verbatim (some clients send a raw token).
|
||||||
|
fn extract_client_credential(
|
||||||
|
authorization: Option<&str>,
|
||||||
|
x_api_key: Option<&str>,
|
||||||
|
) -> Option<String> {
|
||||||
|
// Strip the optional "Bearer " / "Bearer" prefix (case-sensitive, matches
|
||||||
|
// OpenAI SDK behavior) and trim surrounding whitespace before validating
|
||||||
|
// non-empty.
|
||||||
|
let from_authorization = authorization
|
||||||
|
.map(|v| {
|
||||||
|
v.strip_prefix("Bearer ")
|
||||||
|
.or_else(|| v.strip_prefix("Bearer"))
|
||||||
|
.unwrap_or(v)
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.filter(|s| !s.is_empty());
|
||||||
|
if from_authorization.is_some() {
|
||||||
|
return from_authorization;
|
||||||
|
}
|
||||||
|
x_api_key
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::extract_client_credential;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authorization_bearer_strips_prefix() {
|
||||||
|
assert_eq!(
|
||||||
|
extract_client_credential(Some("Bearer sk-abc"), None),
|
||||||
|
Some("sk-abc".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authorization_raw_token_preserved() {
|
||||||
|
// Some clients send the raw token without "Bearer " — accept it.
|
||||||
|
assert_eq!(
|
||||||
|
extract_client_credential(Some("sk-abc"), None),
|
||||||
|
Some("sk-abc".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn x_api_key_used_when_authorization_absent() {
|
||||||
|
assert_eq!(
|
||||||
|
extract_client_credential(None, Some("sk-ant-api-key")),
|
||||||
|
Some("sk-ant-api-key".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authorization_wins_when_both_present() {
|
||||||
|
// If a client is particularly exotic and sends both, prefer the
|
||||||
|
// OpenAI-style Authorization header.
|
||||||
|
assert_eq!(
|
||||||
|
extract_client_credential(Some("Bearer openai-key"), Some("anthropic-key")),
|
||||||
|
Some("openai-key".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_none_when_neither_present() {
|
||||||
|
assert!(extract_client_credential(None, None).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_and_whitespace_headers_are_ignored() {
|
||||||
|
assert!(extract_client_credential(Some(""), None).is_none());
|
||||||
|
assert!(extract_client_credential(Some("Bearer "), None).is_none());
|
||||||
|
assert!(extract_client_credential(Some(" "), Some(" ")).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue