Fix OpenAI family local auth to use bearer (#302)

* Fix OpenAI family local auth to use bearer

* test(gateway): fix bearer auth assertions for openai local flows

* test(usage): make local usage status wait resilient

* style(gateway): apply rustfmt to usage test helper

---------

Co-authored-by: fawney19 <elky0401@gmail.com>
This commit is contained in:
RWDai
2026-04-17 11:10:05 +08:00
committed by GitHub
parent 6964729cb7
commit 96a25d058b
15 changed files with 89 additions and 37 deletions

View File

@@ -5,7 +5,8 @@ use serde_json::Value;
use crate::ai_pipeline::planner::candidate_preparation::resolve_candidate_mapped_model; use crate::ai_pipeline::planner::candidate_preparation::resolve_candidate_mapped_model;
use crate::ai_pipeline::planner::spec_metadata::local_video_create_spec_metadata; use crate::ai_pipeline::planner::spec_metadata::local_video_create_spec_metadata;
use crate::ai_pipeline::transport::auth::{ use crate::ai_pipeline::transport::auth::{
build_passthrough_headers_with_auth, resolve_local_gemini_auth, resolve_local_openai_chat_auth, build_passthrough_headers_with_auth, resolve_local_gemini_auth,
resolve_local_openai_bearer_auth,
}; };
use crate::ai_pipeline::transport::url::{ use crate::ai_pipeline::transport::url::{
build_gemini_video_predict_long_running_url, build_passthrough_path_url, build_gemini_video_predict_long_running_url, build_passthrough_path_url,
@@ -72,7 +73,7 @@ pub(super) async fn resolve_local_video_create_candidate_payload_parts(
} }
let auth = match spec.family { let auth = match spec.family {
LocalVideoCreateFamily::OpenAi => resolve_local_openai_chat_auth(transport), LocalVideoCreateFamily::OpenAi => resolve_local_openai_bearer_auth(transport),
LocalVideoCreateFamily::Gemini => resolve_local_gemini_auth(transport), LocalVideoCreateFamily::Gemini => resolve_local_gemini_auth(transport),
}; };
let Some((auth_header, auth_value)) = auth else { let Some((auth_header, auth_value)) = auth else {

View File

@@ -16,7 +16,8 @@ use crate::ai_pipeline::planner::standard::{
use crate::ai_pipeline::transport::apply_local_header_rules; use crate::ai_pipeline::transport::apply_local_header_rules;
use crate::ai_pipeline::transport::auth::{ use crate::ai_pipeline::transport::auth::{
build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth, build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth,
build_openai_passthrough_headers, ensure_upstream_auth_header, resolve_local_openai_chat_auth, build_openai_passthrough_headers, ensure_upstream_auth_header,
resolve_local_openai_bearer_auth,
}; };
use crate::ai_pipeline::transport::local_openai_chat_transport_unsupported_reason; use crate::ai_pipeline::transport::local_openai_chat_transport_unsupported_reason;
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy, GatewayProviderTransportSnapshot}; use crate::ai_pipeline::{ConversionMode, ExecutionStrategy, GatewayProviderTransportSnapshot};
@@ -76,7 +77,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
planner_state, planner_state,
transport, transport,
candidate, candidate,
resolve_local_openai_chat_auth(transport), resolve_local_openai_bearer_auth(transport),
OauthPreparationContext { OauthPreparationContext {
trace_id, trace_id,
api_format: "openai:chat", api_format: "openai:chat",

View File

@@ -24,7 +24,7 @@ use crate::ai_pipeline::transport::apply_local_header_rules;
use crate::ai_pipeline::transport::auth::{ use crate::ai_pipeline::transport::auth::{
build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth, build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth,
build_openai_passthrough_headers, ensure_upstream_auth_header, resolve_local_gemini_auth, build_openai_passthrough_headers, ensure_upstream_auth_header, resolve_local_gemini_auth,
resolve_local_standard_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth,
}; };
use crate::ai_pipeline::transport::local_standard_transport_unsupported_reason_with_network; use crate::ai_pipeline::transport::local_standard_transport_unsupported_reason_with_network;
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy}; use crate::ai_pipeline::{ConversionMode, ExecutionStrategy};
@@ -107,9 +107,8 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
let direct_auth = if same_format { let direct_auth = if same_format {
match provider_api_format { match provider_api_format {
"gemini:cli" => resolve_local_gemini_auth(transport), "gemini:cli" => resolve_local_gemini_auth(transport),
"claude:cli" | "openai:cli" | "openai:compact" => { "claude:cli" => resolve_local_standard_auth(transport),
resolve_local_standard_auth(transport) "openai:cli" | "openai:compact" => resolve_local_openai_bearer_auth(transport),
}
_ => None, _ => None,
} }
} else { } else {

View File

@@ -202,7 +202,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
let auth = match format_value.as_str() { let auth = match format_value.as_str() {
"openai:chat" => { "openai:chat" => {
crate::provider_transport::auth::resolve_local_openai_chat_auth(&transport) crate::provider_transport::auth::resolve_local_openai_bearer_auth(&transport)
.or(oauth_auth.clone()) .or(oauth_auth.clone())
} }
"claude:chat" => crate::provider_transport::auth::resolve_local_standard_auth(&transport) "claude:chat" => crate::provider_transport::auth::resolve_local_standard_auth(&transport)

View File

@@ -87,7 +87,7 @@ async fn gateway_executes_openai_compact_openai_family_upstream_stream_via_local
endpoint_is_active: true, endpoint_is_active: true,
key_id: "key-openai-compact-finalize-local-1".to_string(), key_id: "key-openai-compact-finalize-local-1".to_string(),
key_name: "prod".to_string(), key_name: "prod".to_string(),
key_auth_type: "bearer".to_string(), key_auth_type: "api_key".to_string(),
key_is_active: true, key_is_active: true,
key_api_formats: Some(vec!["openai:compact".to_string()]), key_api_formats: Some(vec!["openai:compact".to_string()]),
key_allowed_models: None, key_allowed_models: None,
@@ -162,7 +162,7 @@ async fn gateway_executes_openai_compact_openai_family_upstream_stream_via_local
"key-openai-compact-finalize-local-1".to_string(), "key-openai-compact-finalize-local-1".to_string(),
"provider-openai-compact-finalize-local-1".to_string(), "provider-openai-compact-finalize-local-1".to_string(),
"prod".to_string(), "prod".to_string(),
"bearer".to_string(), "api_key".to_string(),
None, None,
true, true,
) )

View File

@@ -449,7 +449,7 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_cli_cross_format_c
model: String, model: String,
stream: bool, stream: bool,
accept: String, accept: String,
api_key: String, authorization: String,
x_client_request_id: String, x_client_request_id: String,
session_id: String, session_id: String,
conversation_id: String, conversation_id: String,
@@ -717,9 +717,9 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_cli_cross_format_c
.and_then(|value| value.as_str()) .and_then(|value| value.as_str())
.unwrap_or_default() .unwrap_or_default()
.to_string(), .to_string(),
api_key: payload authorization: payload
.get("headers") .get("headers")
.and_then(|value| value.get("x-api-key")) .and_then(|value| value.get("authorization"))
.and_then(|value| value.as_str()) .and_then(|value| value.as_str())
.unwrap_or_default() .unwrap_or_default()
.to_string(), .to_string(),
@@ -871,8 +871,8 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_cli_cross_format_c
assert!(seen_execution_runtime_request.stream); assert!(seen_execution_runtime_request.stream);
assert_eq!(seen_execution_runtime_request.accept, "text/event-stream"); assert_eq!(seen_execution_runtime_request.accept, "text/event-stream");
assert_eq!( assert_eq!(
seen_execution_runtime_request.api_key, seen_execution_runtime_request.authorization,
"sk-upstream-openai-chat-cli" "Bearer sk-upstream-openai-chat-cli"
); );
assert_eq!( assert_eq!(
seen_execution_runtime_request.x_client_request_id, seen_execution_runtime_request.x_client_request_id,

View File

@@ -88,7 +88,7 @@ async fn gateway_executes_openai_cli_sync_via_local_decision_gate_with_local_syn
endpoint_is_active: true, endpoint_is_active: true,
key_id: "key-openai-cli-local-1".to_string(), key_id: "key-openai-cli-local-1".to_string(),
key_name: "prod".to_string(), key_name: "prod".to_string(),
key_auth_type: "bearer".to_string(), key_auth_type: "api_key".to_string(),
key_is_active: true, key_is_active: true,
key_api_formats: Some(vec!["openai:cli".to_string()]), key_api_formats: Some(vec!["openai:cli".to_string()]),
key_allowed_models: None, key_allowed_models: None,
@@ -171,7 +171,7 @@ async fn gateway_executes_openai_cli_sync_via_local_decision_gate_with_local_syn
"key-openai-cli-local-1".to_string(), "key-openai-cli-local-1".to_string(),
"provider-openai-cli-local-1".to_string(), "provider-openai-cli-local-1".to_string(),
"prod".to_string(), "prod".to_string(),
"bearer".to_string(), "api_key".to_string(),
None, None,
true, true,
) )

View File

@@ -936,7 +936,7 @@ fn ai_pipeline_video_routes_request_preparation_through_request_payload_seams()
"resolve_candidate_mapped_model(", "resolve_candidate_mapped_model(",
"build_provider_request_body(", "build_provider_request_body(",
"build_video_upstream_url(", "build_video_upstream_url(",
"resolve_local_openai_chat_auth(", "resolve_local_openai_bearer_auth(",
"resolve_local_gemini_auth(", "resolve_local_gemini_auth(",
] { ] {
assert!( assert!(

View File

@@ -803,7 +803,7 @@ fn model_fetch_runtime_paths_depend_on_shared_crates_not_local_pure_helpers() {
"execute_execution_runtime_sync_plan(state, None, &plan)", "execute_execution_runtime_sync_plan(state, None, &plan)",
"resolve_local_standard_auth(", "resolve_local_standard_auth(",
"resolve_local_gemini_auth(", "resolve_local_gemini_auth(",
"resolve_local_openai_chat_auth(", "resolve_local_openai_bearer_auth(",
"resolve_local_vertex_api_key_query_auth(", "resolve_local_vertex_api_key_query_auth(",
"apply_local_header_rules(", "apply_local_header_rules(",
"ensure_upstream_auth_header(", "ensure_upstream_auth_header(",

View File

@@ -33,7 +33,8 @@ where
T: UsageReadRepository + ?Sized, T: UsageReadRepository + ?Sized,
{ {
let mut stored = None; let mut stored = None;
for _ in 0..50 { let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
stored = repository stored = repository
.find_by_request_id(request_id) .find_by_request_id(request_id)
.await .await
@@ -44,9 +45,16 @@ where
{ {
break; break;
} }
if tokio::time::Instant::now() >= deadline {
let observed = stored
.as_ref()
.map(|usage| usage.status.as_str())
.unwrap_or("<missing>");
panic!("usage should reach status {expected_status}, last observed status: {observed}");
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await; tokio::time::sleep(std::time::Duration::from_millis(10)).await;
} }
stored.unwrap_or_else(|| panic!("usage should reach status {expected_status}")) stored.expect("usage should be present once the expected status is observed")
} }
#[tokio::test] #[tokio::test]

View File

@@ -214,7 +214,7 @@ async fn gateway_executes_openai_video_delete_via_reconstructed_data_backed_loca
.to_string(), .to_string(),
authorization: payload authorization: payload
.get("headers") .get("headers")
.and_then(|value| value.get("x-api-key")) .and_then(|value| value.get("authorization"))
.and_then(|value| value.as_str()) .and_then(|value| value.as_str())
.unwrap_or_default() .unwrap_or_default()
.to_string(), .to_string(),
@@ -331,7 +331,7 @@ async fn gateway_executes_openai_video_delete_via_reconstructed_data_backed_loca
); );
assert_eq!( assert_eq!(
seen_execution_runtime_request.authorization, seen_execution_runtime_request.authorization,
"sk-upstream-openai-video" "Bearer sk-upstream-openai-video"
); );
let stored_candidates = request_candidate_repository let stored_candidates = request_candidate_repository

View File

@@ -1,7 +1,7 @@
#![allow(dead_code)] #![allow(dead_code)]
use aether_provider_transport::auth::{ use aether_provider_transport::auth::{
resolve_local_gemini_auth, resolve_local_openai_chat_auth, resolve_local_standard_auth, resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth,
}; };
use aether_provider_transport::policy::{ use aether_provider_transport::policy::{
local_gemini_transport_unsupported_reason_with_network, local_gemini_transport_unsupported_reason_with_network,
@@ -276,11 +276,11 @@ pub fn request_conversion_direct_auth(
.to_ascii_lowercase() .to_ascii_lowercase()
.as_str() .as_str()
{ {
"openai:chat" => resolve_local_openai_chat_auth(transport), "openai:chat" | "openai:cli" | "openai:compact" => {
"gemini:chat" | "gemini:cli" => resolve_local_gemini_auth(transport), resolve_local_openai_bearer_auth(transport)
"openai:cli" | "openai:compact" | "claude:chat" | "claude:cli" => {
resolve_local_standard_auth(transport)
} }
"gemini:chat" | "gemini:cli" => resolve_local_gemini_auth(transport),
"claude:chat" | "claude:cli" => resolve_local_standard_auth(transport),
_ => None, _ => None,
} }
} }

View File

@@ -6,7 +6,7 @@ use aether_provider_transport::antigravity::{
AntigravityRequestAuthSupport, ANTIGRAVITY_REQUEST_USER_AGENT, AntigravityRequestAuthSupport, ANTIGRAVITY_REQUEST_USER_AGENT,
}; };
use aether_provider_transport::auth::{ use aether_provider_transport::auth::{
ensure_upstream_auth_header, resolve_local_gemini_auth, resolve_local_openai_chat_auth, ensure_upstream_auth_header, resolve_local_gemini_auth, resolve_local_openai_bearer_auth,
resolve_local_standard_auth, resolve_local_standard_auth,
}; };
use aether_provider_transport::vertex::resolve_local_vertex_api_key_query_auth; use aether_provider_transport::vertex::resolve_local_vertex_api_key_query_auth;
@@ -309,7 +309,7 @@ async fn resolve_standard_header_auth(
let api_format = transport.endpoint.api_format.trim().to_ascii_lowercase(); let api_format = transport.endpoint.api_format.trim().to_ascii_lowercase();
if api_format.starts_with("openai:") { if api_format.starts_with("openai:") {
return Ok(resolve_local_openai_chat_auth(transport)); return Ok(resolve_local_openai_bearer_auth(transport));
} }
if api_format.starts_with("claude:") { if api_format.starts_with("claude:") {
return Ok(resolve_local_standard_auth(transport)); return Ok(resolve_local_standard_auth(transport));
@@ -337,7 +337,7 @@ async fn resolve_bearer_or_oauth_header_auth(
return Ok(Some(auth)); return Ok(Some(auth));
} }
if let Some((name, value)) = resolve_local_openai_chat_auth(transport) { if let Some((name, value)) = resolve_local_openai_bearer_auth(transport) {
return Ok(Some((name, value))); return Ok(Some((name, value)));
} }
@@ -599,6 +599,25 @@ mod tests {
); );
} }
#[tokio::test]
async fn builds_openai_compact_models_fetch_plan_with_bearer_authorization() {
let runtime = TestRuntime {
oauth_auth: None,
proxy: None,
};
let mut transport = sample_transport("openai", "openai:compact", "api_key");
transport.key.decrypted_auth_config = None;
let plan = build_models_fetch_execution_plan(&runtime, &transport)
.await
.expect("plan");
assert_eq!(plan.url, "https://example.com/v1/models");
assert_eq!(
plan.headers.get("authorization").map(String::as_str),
Some("Bearer secret")
);
}
#[tokio::test] #[tokio::test]
async fn builds_claude_models_fetch_plan_with_pagination() { async fn builds_claude_models_fetch_plan_with_pagination() {
let runtime = TestRuntime { let runtime = TestRuntime {

View File

@@ -227,7 +227,7 @@ fn merge_comma_header_values(left: Option<&str>, right: Option<&str>) -> Option<
} }
} }
pub fn resolve_local_openai_chat_auth( pub fn resolve_local_openai_bearer_auth(
transport: &GatewayProviderTransportSnapshot, transport: &GatewayProviderTransportSnapshot,
) -> Option<(String, String)> { ) -> Option<(String, String)> {
let auth_type = transport.key.auth_type.trim().to_ascii_lowercase(); let auth_type = transport.key.auth_type.trim().to_ascii_lowercase();
@@ -274,7 +274,7 @@ fn resolved_local_secret(transport: &GatewayProviderTransportSnapshot) -> Option
mod tests { mod tests {
use super::{ use super::{
build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth, build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth,
resolve_local_standard_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth,
}; };
use crate::snapshot::{ use crate::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey, GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
@@ -442,4 +442,28 @@ mod tests {
fn local_standard_auth_rejects_placeholder_secret() { fn local_standard_auth_rejects_placeholder_secret() {
assert!(resolve_local_standard_auth(&sample_transport()).is_none()); assert!(resolve_local_standard_auth(&sample_transport()).is_none());
} }
#[test]
fn local_openai_bearer_auth_maps_api_key_to_bearer_authorization() {
let mut transport = sample_transport();
transport.key.auth_type = "api_key".to_string();
transport.key.decrypted_api_key = "sk-openai".to_string();
assert_eq!(
resolve_local_openai_bearer_auth(&transport),
Some(("authorization".to_string(), "Bearer sk-openai".to_string(),))
);
}
#[test]
fn local_openai_bearer_auth_preserves_bearer_header_shape() {
let mut transport = sample_transport();
transport.key.auth_type = "bearer".to_string();
transport.key.decrypted_api_key = "sk-openai".to_string();
assert_eq!(
resolve_local_openai_bearer_auth(&transport),
Some(("authorization".to_string(), "Bearer sk-openai".to_string(),))
);
}
} }

View File

@@ -4,7 +4,7 @@ use aether_video_tasks_core::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use super::auth::{resolve_local_gemini_auth, resolve_local_standard_auth}; use super::auth::{resolve_local_gemini_auth, resolve_local_openai_bearer_auth};
use super::network::resolve_transport_execution_timeouts; use super::network::resolve_transport_execution_timeouts;
use super::policy::{supports_local_gemini_transport, supports_local_standard_transport}; use super::policy::{supports_local_gemini_transport, supports_local_standard_transport};
use super::snapshot::GatewayProviderTransportSnapshot; use super::snapshot::GatewayProviderTransportSnapshot;
@@ -30,7 +30,7 @@ pub fn resolve_local_video_task_transport(
if !supports_local_standard_transport(transport, api_format) { if !supports_local_standard_transport(transport, api_format) {
return None; return None;
} }
resolve_local_standard_auth(transport)? resolve_local_openai_bearer_auth(transport)?
} }
"gemini:video" => { "gemini:video" => {
if !supports_local_gemini_transport(transport, api_format) { if !supports_local_gemini_transport(transport, api_format) {
@@ -229,7 +229,7 @@ mod tests {
#[test] #[test]
fn resolves_openai_video_transport() { fn resolves_openai_video_transport() {
let transport = resolve_local_video_task_transport( let transport = resolve_local_video_task_transport(
&sample_transport("openai:video", "bearer"), &sample_transport("openai:video", "api_key"),
"openai:video", "openai:video",
Some("sora".to_string()), Some("sora".to_string()),
) )