mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
fix(provider): 将rust分支的gemini cli端点行为对齐到python分支 (#321)
* fix(provider): 对齐 Vertex/Gemini 上游发包与 Python master - provider-transport: 为 custom+aiplatform 推断 Vertex API key 上下文并统一 URL 构建顺序,复用共享 request_url 构建最终上游地址 - ai-pipeline/gateway: Vertex Gemini 路径改为仅使用 URL query key,不再向上游附带 x-goog-api-key header;同步对齐 standard/admin/test-connection/runtime miss 摘要中的最终 URL - gemini conversion: 按 Python master 输出 Gemini 请求体,补齐 system_instruction / generation_config / tool_config / function_declarations 形态,并移植 Gemini schema 清洗逻辑 - scheduler/executor: 将最终 upstream_url、mapped_model、key_name 写入候选 extra_data,运行时 miss 诊断优先展示真实展开后的上游 URL 便于服务器排障 * fix(provider): 修复 Vertex provider 测试与本地调度链路 * fix(provider): 对齐 Vertex 本地执行与 Rust CI
This commit is contained in:
@@ -10,6 +10,7 @@ mod network;
|
||||
pub mod oauth_refresh;
|
||||
pub mod policy;
|
||||
pub mod provider_types;
|
||||
mod request_url;
|
||||
pub mod rules;
|
||||
pub mod snapshot;
|
||||
pub mod url;
|
||||
@@ -40,6 +41,7 @@ pub use policy::{
|
||||
local_standard_transport_unsupported_reason_with_network, supports_local_gemini_transport,
|
||||
supports_local_gemini_transport_with_network, supports_local_standard_transport,
|
||||
};
|
||||
pub use request_url::{build_transport_request_url, TransportRequestUrlParams};
|
||||
pub use rules::{
|
||||
apply_local_body_rules, apply_local_header_rules, body_rules_are_locally_supported,
|
||||
body_rules_handle_path, header_rules_are_locally_supported,
|
||||
@@ -48,6 +50,7 @@ pub use snapshot::{
|
||||
read_provider_transport_snapshot, GatewayProviderTransportSnapshot,
|
||||
ProviderTransportSnapshotSource,
|
||||
};
|
||||
pub use vertex::{is_vertex_api_key_transport_context, uses_vertex_api_key_query_auth};
|
||||
pub use video::{
|
||||
reconstruct_local_video_task_snapshot, resolve_local_video_task_transport,
|
||||
VideoTaskTransportSnapshotLookup,
|
||||
|
||||
399
crates/aether-provider-transport/src/request_url.rs
Normal file
399
crates/aether-provider-transport/src/request_url.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use regex::Regex;
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::antigravity::{build_antigravity_v1internal_url, AntigravityRequestUrlAction};
|
||||
use crate::claude_code::build_claude_code_messages_url;
|
||||
use crate::snapshot::GatewayProviderTransportSnapshot;
|
||||
use crate::url::{
|
||||
build_claude_messages_url, build_gemini_content_url, build_openai_chat_url,
|
||||
build_openai_cli_url, build_passthrough_path_url,
|
||||
};
|
||||
use crate::vertex::{
|
||||
build_vertex_api_key_gemini_content_url, resolve_local_vertex_api_key_query_auth,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TransportRequestUrlParams<'a> {
|
||||
pub provider_api_format: &'a str,
|
||||
pub mapped_model: Option<&'a str>,
|
||||
pub upstream_is_stream: bool,
|
||||
pub request_query: Option<&'a str>,
|
||||
pub kiro_api_region: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn build_transport_request_url(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
params: TransportRequestUrlParams<'_>,
|
||||
) -> Option<String> {
|
||||
if let Some(url) = build_transport_hook_url(transport, params) {
|
||||
return Some(url);
|
||||
}
|
||||
|
||||
let provider_api_format = params.provider_api_format.trim().to_ascii_lowercase();
|
||||
let custom_path = transport
|
||||
.endpoint
|
||||
.custom_path
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|path| expand_custom_path_template(path, build_path_params(params)));
|
||||
|
||||
if let Some(path) = custom_path.as_deref() {
|
||||
let blocked_keys = if provider_api_format.starts_with("gemini:") {
|
||||
&["key"][..]
|
||||
} else {
|
||||
&[][..]
|
||||
};
|
||||
let url = build_passthrough_path_url(
|
||||
&transport.endpoint.base_url,
|
||||
path,
|
||||
params.request_query,
|
||||
blocked_keys,
|
||||
)?;
|
||||
return Some(maybe_add_gemini_stream_alt_sse(
|
||||
url,
|
||||
&provider_api_format,
|
||||
params.upstream_is_stream,
|
||||
));
|
||||
}
|
||||
|
||||
let url = match provider_api_format.as_str() {
|
||||
"openai:chat" => Some(build_openai_chat_url(
|
||||
&transport.endpoint.base_url,
|
||||
params.request_query,
|
||||
)),
|
||||
"openai:cli" => Some(build_openai_cli_url(
|
||||
&transport.endpoint.base_url,
|
||||
params.request_query,
|
||||
false,
|
||||
)),
|
||||
"openai:compact" => Some(build_openai_cli_url(
|
||||
&transport.endpoint.base_url,
|
||||
params.request_query,
|
||||
true,
|
||||
)),
|
||||
"claude:chat" | "claude:cli" => Some(build_claude_messages_url(
|
||||
&transport.endpoint.base_url,
|
||||
params.request_query,
|
||||
)),
|
||||
"gemini:chat" | "gemini:cli" => build_gemini_content_url(
|
||||
&transport.endpoint.base_url,
|
||||
params.mapped_model?,
|
||||
params.upstream_is_stream,
|
||||
params.request_query,
|
||||
),
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
Some(maybe_add_gemini_stream_alt_sse(
|
||||
url,
|
||||
&provider_api_format,
|
||||
params.upstream_is_stream,
|
||||
))
|
||||
}
|
||||
|
||||
fn build_transport_hook_url(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
params: TransportRequestUrlParams<'_>,
|
||||
) -> Option<String> {
|
||||
if let Some(api_region) = params.kiro_api_region {
|
||||
return crate::kiro::build_kiro_generate_assistant_response_url(
|
||||
&transport.endpoint.base_url,
|
||||
params.request_query,
|
||||
Some(api_region),
|
||||
);
|
||||
}
|
||||
|
||||
if transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("claude_code")
|
||||
{
|
||||
return Some(build_claude_code_messages_url(
|
||||
&transport.endpoint.base_url,
|
||||
params.request_query,
|
||||
));
|
||||
}
|
||||
|
||||
if params
|
||||
.provider_api_format
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.starts_with("gemini:")
|
||||
{
|
||||
if let Some(auth) = resolve_local_vertex_api_key_query_auth(transport) {
|
||||
return build_vertex_api_key_gemini_content_url(
|
||||
params.mapped_model?,
|
||||
params.upstream_is_stream,
|
||||
&auth.value,
|
||||
params.request_query,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("antigravity")
|
||||
{
|
||||
let query = params.request_query.map(|raw| {
|
||||
form_urlencoded::parse(raw.as_bytes())
|
||||
.into_owned()
|
||||
.collect::<BTreeMap<String, String>>()
|
||||
});
|
||||
return build_antigravity_v1internal_url(
|
||||
&transport.endpoint.base_url,
|
||||
if params.upstream_is_stream {
|
||||
AntigravityRequestUrlAction::StreamGenerateContent
|
||||
} else {
|
||||
AntigravityRequestUrlAction::GenerateContent
|
||||
},
|
||||
query.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn build_path_params(params: TransportRequestUrlParams<'_>) -> BTreeMap<&'static str, &str> {
|
||||
let mut path_params = BTreeMap::new();
|
||||
if let Some(model) = params
|
||||
.mapped_model
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
path_params.insert("model", model);
|
||||
}
|
||||
if params
|
||||
.provider_api_format
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.starts_with("gemini:")
|
||||
{
|
||||
path_params.insert(
|
||||
"action",
|
||||
if params.upstream_is_stream {
|
||||
"streamGenerateContent"
|
||||
} else {
|
||||
"generateContent"
|
||||
},
|
||||
);
|
||||
}
|
||||
path_params
|
||||
}
|
||||
|
||||
fn expand_custom_path_template(path: &str, params: BTreeMap<&'static str, &str>) -> String {
|
||||
if params.is_empty() {
|
||||
return path.to_string();
|
||||
}
|
||||
|
||||
let regex = custom_path_template_regex();
|
||||
let mut missing_key = false;
|
||||
let replaced = regex.replace_all(path, |captures: ®ex::Captures<'_>| {
|
||||
let key = captures
|
||||
.get(1)
|
||||
.map(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
match params.get(key).copied() {
|
||||
Some(value) => value.to_string(),
|
||||
None => {
|
||||
missing_key = true;
|
||||
captures
|
||||
.get(0)
|
||||
.map(|value| value.as_str().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if missing_key {
|
||||
path.to_string()
|
||||
} else {
|
||||
replaced.into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_add_gemini_stream_alt_sse(
|
||||
upstream_url: String,
|
||||
provider_api_format: &str,
|
||||
upstream_is_stream: bool,
|
||||
) -> String {
|
||||
if !provider_api_format.starts_with("gemini:") || !upstream_is_stream {
|
||||
return upstream_url;
|
||||
}
|
||||
|
||||
let has_alt = upstream_url
|
||||
.split_once('?')
|
||||
.map(|(_, query)| {
|
||||
form_urlencoded::parse(query.as_bytes())
|
||||
.any(|(key, _)| key.as_ref().eq_ignore_ascii_case("alt"))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if has_alt {
|
||||
return upstream_url;
|
||||
}
|
||||
|
||||
if upstream_url.contains('?') {
|
||||
format!("{upstream_url}&alt=sse")
|
||||
} else {
|
||||
format!("{upstream_url}?alt=sse")
|
||||
}
|
||||
}
|
||||
|
||||
fn custom_path_template_regex() -> &'static Regex {
|
||||
static REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
REGEX.get_or_init(|| {
|
||||
Regex::new(r"\{([A-Za-z_][A-Za-z0-9_]*)\}")
|
||||
.expect("custom_path template regex should compile")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{build_transport_request_url, TransportRequestUrlParams};
|
||||
use crate::snapshot::{
|
||||
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
|
||||
fn sample_transport(
|
||||
provider_type: &str,
|
||||
api_format: &str,
|
||||
base_url: &str,
|
||||
custom_path: Option<&str>,
|
||||
) -> GatewayProviderTransportSnapshot {
|
||||
GatewayProviderTransportSnapshot {
|
||||
provider: GatewayProviderTransportProvider {
|
||||
id: "provider-1".to_string(),
|
||||
name: "provider".to_string(),
|
||||
provider_type: provider_type.to_string(),
|
||||
website: None,
|
||||
is_active: true,
|
||||
keep_priority_on_conversion: false,
|
||||
enable_format_conversion: false,
|
||||
concurrent_limit: None,
|
||||
max_retries: None,
|
||||
proxy: None,
|
||||
request_timeout_secs: None,
|
||||
stream_first_byte_timeout_secs: None,
|
||||
config: None,
|
||||
},
|
||||
endpoint: GatewayProviderTransportEndpoint {
|
||||
id: "endpoint-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
api_format: api_format.to_string(),
|
||||
api_family: None,
|
||||
endpoint_kind: None,
|
||||
is_active: true,
|
||||
base_url: base_url.to_string(),
|
||||
header_rules: None,
|
||||
body_rules: None,
|
||||
max_retries: None,
|
||||
custom_path: custom_path.map(ToOwned::to_owned),
|
||||
config: None,
|
||||
format_acceptance_config: None,
|
||||
proxy: None,
|
||||
},
|
||||
key: GatewayProviderTransportKey {
|
||||
id: "key-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
name: "key".to_string(),
|
||||
auth_type: "api_key".to_string(),
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
global_priority_by_format: None,
|
||||
expires_at_unix_secs: None,
|
||||
proxy: None,
|
||||
fingerprint: None,
|
||||
decrypted_api_key: "vertex-secret".to_string(),
|
||||
decrypted_auth_config: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uses_vertex_hook_before_custom_path_for_custom_aiplatform_transport() {
|
||||
let transport = sample_transport(
|
||||
"custom",
|
||||
"gemini:cli",
|
||||
"https://aiplatform.googleapis.com",
|
||||
Some("/custom/{model}:{action}"),
|
||||
);
|
||||
|
||||
let url = build_transport_request_url(
|
||||
&transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "gemini:cli",
|
||||
mapped_model: Some("gemini-3.1-pro-preview"),
|
||||
upstream_is_stream: true,
|
||||
request_query: Some("foo=bar"),
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.expect("vertex hook url");
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://aiplatform.googleapis.com/v1/publishers/google/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse&foo=bar&key=vertex-secret"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expands_custom_path_templates_when_hook_does_not_apply() {
|
||||
let transport = sample_transport(
|
||||
"custom",
|
||||
"gemini:chat",
|
||||
"https://generativelanguage.googleapis.com",
|
||||
Some("/v1beta/models/{model}:{action}"),
|
||||
);
|
||||
|
||||
let url = build_transport_request_url(
|
||||
&transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "gemini:chat",
|
||||
mapped_model: Some("gemini-2.5-pro"),
|
||||
upstream_is_stream: false,
|
||||
request_query: Some("key=client-key&foo=bar"),
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.expect("expanded custom path url");
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?foo=bar"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_original_custom_path_when_template_params_are_missing() {
|
||||
let transport = sample_transport(
|
||||
"custom",
|
||||
"claude:chat",
|
||||
"https://api.example.com",
|
||||
Some("/v1/messages/{model}"),
|
||||
);
|
||||
|
||||
let url = build_transport_request_url(
|
||||
&transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "claude:chat",
|
||||
mapped_model: None,
|
||||
upstream_is_stream: false,
|
||||
request_query: None,
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.expect("fallback custom path url");
|
||||
|
||||
assert_eq!(url, "https://api.example.com/v1/messages/{model}");
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
mod auth;
|
||||
mod context;
|
||||
mod policy;
|
||||
mod url;
|
||||
|
||||
pub use auth::{
|
||||
resolve_local_vertex_api_key_query_auth, VertexApiKeyQueryAuth, VERTEX_API_KEY_QUERY_PARAM,
|
||||
};
|
||||
pub use context::{
|
||||
is_vertex_api_key_transport_context, looks_like_vertex_ai_host, uses_vertex_api_key_query_auth,
|
||||
};
|
||||
pub use policy::{
|
||||
local_vertex_api_key_gemini_transport_unsupported_reason_with_network,
|
||||
supports_local_vertex_api_key_gemini_transport,
|
||||
|
||||
@@ -11,12 +11,7 @@ pub struct VertexApiKeyQueryAuth {
|
||||
pub fn resolve_local_vertex_api_key_query_auth(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> Option<VertexApiKeyQueryAuth> {
|
||||
if !transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(super::PROVIDER_TYPE)
|
||||
{
|
||||
if !super::is_vertex_api_key_transport_context(transport) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -127,4 +122,15 @@ mod tests {
|
||||
transport.key.decrypted_auth_config = Some("{\"project_id\":\"demo-project\"}".to_string());
|
||||
assert!(resolve_local_vertex_api_key_query_auth(&transport).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_query_auth_for_custom_aiplatform_transport() {
|
||||
let mut transport = sample_transport();
|
||||
transport.provider.provider_type = "custom".to_string();
|
||||
transport.endpoint.api_format = "gemini:cli".to_string();
|
||||
|
||||
let auth = resolve_local_vertex_api_key_query_auth(&transport)
|
||||
.expect("custom aiplatform transport should resolve");
|
||||
assert_eq!(auth.value, "vertex-secret");
|
||||
}
|
||||
}
|
||||
|
||||
163
crates/aether-provider-transport/src/vertex/context.rs
Normal file
163
crates/aether-provider-transport/src/vertex/context.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use url::Url;
|
||||
|
||||
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
||||
|
||||
const VERTEX_AI_HOST: &str = "aiplatform.googleapis.com";
|
||||
|
||||
pub fn looks_like_vertex_ai_host(base_url: &str) -> bool {
|
||||
let trimmed = base_url.trim();
|
||||
if trimmed.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(parsed) = Url::parse(trimmed) else {
|
||||
return false;
|
||||
};
|
||||
let Some(host) = parsed
|
||||
.host_str()
|
||||
.map(|value| value.trim().to_ascii_lowercase())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
host == VERTEX_AI_HOST
|
||||
|| host.ends_with(&format!(".{VERTEX_AI_HOST}"))
|
||||
|| host.ends_with(&format!("-{VERTEX_AI_HOST}"))
|
||||
}
|
||||
|
||||
pub fn is_vertex_api_key_transport_context(transport: &GatewayProviderTransportSnapshot) -> bool {
|
||||
if transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(super::PROVIDER_TYPE)
|
||||
{
|
||||
return transport
|
||||
.key
|
||||
.auth_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("api_key");
|
||||
}
|
||||
|
||||
if !looks_like_vertex_ai_host(&transport.endpoint.base_url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let endpoint_api_format = transport.endpoint.api_format.trim().to_ascii_lowercase();
|
||||
if !endpoint_api_format.starts_with("gemini:") && !endpoint_api_format.starts_with("claude:") {
|
||||
return false;
|
||||
}
|
||||
|
||||
transport
|
||||
.key
|
||||
.auth_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("api_key")
|
||||
}
|
||||
|
||||
pub fn uses_vertex_api_key_query_auth(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
provider_api_format: &str,
|
||||
) -> bool {
|
||||
is_vertex_api_key_transport_context(transport)
|
||||
&& provider_api_format
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.starts_with("gemini:")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
is_vertex_api_key_transport_context, looks_like_vertex_ai_host,
|
||||
uses_vertex_api_key_query_auth,
|
||||
};
|
||||
use crate::snapshot::{
|
||||
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
|
||||
fn sample_transport() -> GatewayProviderTransportSnapshot {
|
||||
GatewayProviderTransportSnapshot {
|
||||
provider: GatewayProviderTransportProvider {
|
||||
id: "provider-1".to_string(),
|
||||
name: "Vertex".to_string(),
|
||||
provider_type: "custom".to_string(),
|
||||
website: None,
|
||||
is_active: true,
|
||||
keep_priority_on_conversion: false,
|
||||
enable_format_conversion: false,
|
||||
concurrent_limit: None,
|
||||
max_retries: None,
|
||||
proxy: None,
|
||||
request_timeout_secs: None,
|
||||
stream_first_byte_timeout_secs: None,
|
||||
config: None,
|
||||
},
|
||||
endpoint: GatewayProviderTransportEndpoint {
|
||||
id: "endpoint-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
api_format: "gemini:cli".to_string(),
|
||||
api_family: Some("gemini".to_string()),
|
||||
endpoint_kind: Some("cli".to_string()),
|
||||
is_active: true,
|
||||
base_url: "https://aiplatform.googleapis.com".to_string(),
|
||||
header_rules: None,
|
||||
body_rules: None,
|
||||
max_retries: None,
|
||||
custom_path: Some("/v1/publishers/google/models/{model}:{action}".to_string()),
|
||||
config: None,
|
||||
format_acceptance_config: None,
|
||||
proxy: None,
|
||||
},
|
||||
key: GatewayProviderTransportKey {
|
||||
id: "key-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
name: "key".to_string(),
|
||||
auth_type: "api_key".to_string(),
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["gemini:cli".to_string()]),
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
global_priority_by_format: None,
|
||||
expires_at_unix_secs: None,
|
||||
proxy: None,
|
||||
fingerprint: None,
|
||||
decrypted_api_key: "vertex-secret".to_string(),
|
||||
decrypted_auth_config: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_vertex_host() {
|
||||
assert!(looks_like_vertex_ai_host(
|
||||
"https://aiplatform.googleapis.com"
|
||||
));
|
||||
assert!(looks_like_vertex_ai_host(
|
||||
"https://us-central1-aiplatform.googleapis.com"
|
||||
));
|
||||
assert!(!looks_like_vertex_ai_host("https://example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infers_vertex_api_key_context_for_custom_aiplatform_transport() {
|
||||
assert!(is_vertex_api_key_transport_context(&sample_transport()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_api_key_custom_aiplatform_transport() {
|
||||
let mut transport = sample_transport();
|
||||
transport.key.auth_type = "bearer".to_string();
|
||||
assert!(!is_vertex_api_key_transport_context(&transport));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_vertex_query_auth_usage_for_gemini_formats() {
|
||||
let transport = sample_transport();
|
||||
assert!(uses_vertex_api_key_query_auth(&transport, "gemini:cli"));
|
||||
assert!(uses_vertex_api_key_query_auth(&transport, "gemini:chat"));
|
||||
assert!(!uses_vertex_api_key_query_auth(&transport, "claude:chat"));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,15 @@ use super::super::{
|
||||
};
|
||||
use super::auth::resolve_local_vertex_api_key_query_auth;
|
||||
|
||||
fn is_vertex_transport_family(transport: &GatewayProviderTransportSnapshot) -> bool {
|
||||
transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(super::PROVIDER_TYPE)
|
||||
|| super::looks_like_vertex_ai_host(&transport.endpoint.base_url)
|
||||
}
|
||||
|
||||
pub fn local_vertex_api_key_gemini_transport_unsupported_reason_with_network(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> Option<&'static str> {
|
||||
@@ -18,19 +27,21 @@ pub fn local_vertex_api_key_gemini_transport_unsupported_reason_with_network(
|
||||
};
|
||||
}
|
||||
if !transport
|
||||
.provider
|
||||
.provider_type
|
||||
.endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(super::PROVIDER_TYPE)
|
||||
{
|
||||
return Some("transport_provider_type_unsupported");
|
||||
}
|
||||
let endpoint_api_format = transport.endpoint.api_format.trim();
|
||||
if !endpoint_api_format.eq_ignore_ascii_case("gemini:chat")
|
||||
&& !endpoint_api_format.eq_ignore_ascii_case("gemini:cli")
|
||||
.eq_ignore_ascii_case("gemini:chat")
|
||||
&& !transport
|
||||
.endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("gemini:cli")
|
||||
{
|
||||
return Some("transport_api_format_mismatch");
|
||||
}
|
||||
if !is_vertex_transport_family(transport) {
|
||||
return Some("transport_provider_type_unsupported");
|
||||
}
|
||||
if !header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref()) {
|
||||
return Some("transport_header_rules_unsupported");
|
||||
}
|
||||
@@ -87,18 +98,21 @@ fn supports_local_vertex_api_key_same_format_transport(
|
||||
return false;
|
||||
}
|
||||
if !transport
|
||||
.provider
|
||||
.provider_type
|
||||
.endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(super::PROVIDER_TYPE)
|
||||
.eq_ignore_ascii_case(api_formats[0])
|
||||
&& !api_formats.iter().any(|api_format| {
|
||||
transport
|
||||
.endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(api_format)
|
||||
})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let endpoint_api_format = transport.endpoint.api_format.trim();
|
||||
if !api_formats
|
||||
.iter()
|
||||
.any(|api_format| endpoint_api_format.eq_ignore_ascii_case(api_format))
|
||||
{
|
||||
if !super::is_vertex_api_key_transport_context(transport) {
|
||||
return false;
|
||||
}
|
||||
if !header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref())
|
||||
@@ -223,6 +237,14 @@ mod tests {
|
||||
assert!(supports_local_vertex_api_key_gemini_transport(&transport));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_custom_aiplatform_gemini_cli_subset() {
|
||||
let mut transport = sample_transport();
|
||||
transport.provider.provider_type = "custom".to_string();
|
||||
transport.endpoint.api_format = "gemini:cli".to_string();
|
||||
assert!(supports_local_vertex_api_key_gemini_transport(&transport));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_vertex_service_account_subset() {
|
||||
let mut transport = sample_transport();
|
||||
|
||||
Reference in New Issue
Block a user