mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
fix(gateway): restore HTTP and WS upstream support
This commit is contained in:
@@ -21,10 +21,7 @@ use aether_contracts::{
|
|||||||
TRANSPORT_HTTP_MODE_H2C_PRIOR_KNOWLEDGE, TRANSPORT_HTTP_MODE_HTTP1_ONLY,
|
TRANSPORT_HTTP_MODE_H2C_PRIOR_KNOWLEDGE, TRANSPORT_HTTP_MODE_HTTP1_ONLY,
|
||||||
};
|
};
|
||||||
use aether_data::repository::proxy_nodes::ProxyNodeTrafficMutation;
|
use aether_data::repository::proxy_nodes::ProxyNodeTrafficMutation;
|
||||||
use aether_http::{
|
use aether_http::{apply_http_client_config, is_private_or_reserved_ip, HttpClientConfig};
|
||||||
apply_http_client_config, is_https_or_loopback_http_url, is_private_or_reserved_ip,
|
|
||||||
HttpClientConfig,
|
|
||||||
};
|
|
||||||
use aether_runtime::{MetricKind, MetricSample};
|
use aether_runtime::{MetricKind, MetricSample};
|
||||||
use axum::body::Bytes;
|
use axum::body::Bytes;
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
@@ -5173,11 +5170,6 @@ fn validate_execution_upstream_url(
|
|||||||
"upstream URL must not include a fragment".to_string(),
|
"upstream URL must not include a fragment".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !is_https_or_loopback_http_url(&url) {
|
|
||||||
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
||||||
"remote upstream URL must use HTTPS".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let literal_ip = match url.host() {
|
let literal_ip = match url.host() {
|
||||||
Some(url::Host::Ipv4(address)) => Some(IpAddr::V4(address)),
|
Some(url::Host::Ipv4(address)) => Some(IpAddr::V4(address)),
|
||||||
Some(url::Host::Ipv6(address)) => Some(IpAddr::V6(address)),
|
Some(url::Host::Ipv6(address)) => Some(IpAddr::V6(address)),
|
||||||
@@ -5389,9 +5381,13 @@ mod tests {
|
|||||||
const RELAY_TEST_SECRET: &str = "relay-test-secret-at-least-32-bytes";
|
const RELAY_TEST_SECRET: &str = "relay-test-secret-at-least-32-bytes";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn execution_upstream_url_requires_https_or_literal_loopback_http() {
|
fn execution_upstream_url_accepts_http_and_https_with_safe_targets() {
|
||||||
for allowed in [
|
for allowed in [
|
||||||
"https://api.example.test/v1/responses?api-version=1",
|
"https://api.example.test/v1/responses?api-version=1",
|
||||||
|
"http://api.example.test:8080/v1/responses?api-version=1",
|
||||||
|
"http://8.8.8.8:8080/v1/responses",
|
||||||
|
"https://8.8.8.8/v1/responses",
|
||||||
|
"http://[2606:4700:4700::1111]:8080/v1/responses",
|
||||||
"http://localhost:8080/v1/responses",
|
"http://localhost:8080/v1/responses",
|
||||||
"http://127.42.0.1:8080/v1/responses",
|
"http://127.42.0.1:8080/v1/responses",
|
||||||
"http://[::1]:8080/v1/responses",
|
"http://[::1]:8080/v1/responses",
|
||||||
@@ -5403,7 +5399,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for rejected in [
|
for rejected in [
|
||||||
"http://api.example.test/v1/responses",
|
|
||||||
"http://10.0.0.1/v1/responses",
|
"http://10.0.0.1/v1/responses",
|
||||||
"http://0.0.0.0:8080/v1/responses",
|
"http://0.0.0.0:8080/v1/responses",
|
||||||
"http://[::ffff:127.0.0.1]:8080/v1/responses",
|
"http://[::ffff:127.0.0.1]:8080/v1/responses",
|
||||||
@@ -5411,6 +5406,8 @@ mod tests {
|
|||||||
"https://10.0.0.1:8443/v1/responses",
|
"https://10.0.0.1:8443/v1/responses",
|
||||||
"https://[email protected]/v1/responses",
|
"https://[email protected]/v1/responses",
|
||||||
"https://example.test/v1/responses#secret",
|
"https://example.test/v1/responses#secret",
|
||||||
|
"http://[email protected]/v1/responses",
|
||||||
|
"http://example.test/v1/responses#secret",
|
||||||
"ftp://localhost/resource",
|
"ftp://localhost/resource",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -157,16 +157,22 @@ pub(crate) fn websocket_upstream_url(
|
|||||||
return Err(invalid_code);
|
return Err(invalid_code);
|
||||||
}
|
}
|
||||||
let websocket_scheme = match url.scheme() {
|
let websocket_scheme = match url.scheme() {
|
||||||
"https" => "wss",
|
"https" | "wss" => "wss",
|
||||||
"http" => "ws",
|
"http" | "ws" => "ws",
|
||||||
"wss" => return Ok(url),
|
|
||||||
"ws" if aether_http::url_has_literal_loopback_host(&url) => return Ok(url),
|
|
||||||
"ws" => return Err(invalid_code),
|
|
||||||
_ => return Err(invalid_code),
|
_ => return Err(invalid_code),
|
||||||
};
|
};
|
||||||
url.set_scheme(websocket_scheme).map_err(|_| invalid_code)?;
|
url.set_scheme(websocket_scheme).map_err(|_| invalid_code)?;
|
||||||
if url.scheme() == "ws" && !aether_http::url_has_literal_loopback_host(&url) {
|
if url.scheme() == "ws" {
|
||||||
return Err(invalid_code);
|
let literal_ip = match url.host() {
|
||||||
|
Some(url::Host::Ipv4(address)) => Some(std::net::IpAddr::V4(address)),
|
||||||
|
Some(url::Host::Ipv6(address)) => Some(std::net::IpAddr::V6(address)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if literal_ip.is_some_and(|address| {
|
||||||
|
aether_http::is_private_or_reserved_ip(address) && !address.is_loopback()
|
||||||
|
}) {
|
||||||
|
return Err(invalid_code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
@@ -844,15 +850,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn maps_http_url_to_websocket_url_without_losing_path_or_query() {
|
fn maps_http_url_to_websocket_url_without_losing_path_or_query() {
|
||||||
let url = websocket_upstream_url(
|
for (http_scheme, websocket_scheme) in [("https", "wss"), ("http", "ws")] {
|
||||||
"https://example.test/backend-api/codex/responses?x=1",
|
let url = websocket_upstream_url(
|
||||||
"invalid",
|
&format!("{http_scheme}://example.test:8080/backend-api/codex/responses?x=1"),
|
||||||
)
|
"invalid",
|
||||||
.expect("URL should be converted");
|
)
|
||||||
assert_eq!(
|
.expect("URL should be converted");
|
||||||
url.as_str(),
|
assert_eq!(
|
||||||
"wss://example.test/backend-api/codex/responses?x=1"
|
url.as_str(),
|
||||||
);
|
format!("{websocket_scheme}://example.test:8080/backend-api/codex/responses?x=1")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -861,10 +869,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remote_websocket_requires_wss_but_loopback_ws_is_allowed() {
|
fn websocket_upstream_url_accepts_ws_and_wss_with_safe_targets() {
|
||||||
for allowed in [
|
for allowed in [
|
||||||
"wss://example.test/v1/responses",
|
"wss://example.test/v1/responses",
|
||||||
"https://example.test/v1/responses",
|
"https://example.test/v1/responses",
|
||||||
|
"ws://example.test:8080/v1/responses",
|
||||||
|
"http://example.test:8080/v1/responses",
|
||||||
|
"http://8.8.8.8:8080/v1/responses",
|
||||||
|
"ws://[2606:4700:4700::1111]:8080/v1/responses",
|
||||||
"ws://localhost:8080/v1/responses",
|
"ws://localhost:8080/v1/responses",
|
||||||
"http://127.42.0.1:8080/v1/responses",
|
"http://127.42.0.1:8080/v1/responses",
|
||||||
"ws://[::1]:8080/v1/responses",
|
"ws://[::1]:8080/v1/responses",
|
||||||
@@ -875,11 +887,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
for rejected in [
|
for rejected in [
|
||||||
"ws://example.test/v1/responses",
|
|
||||||
"http://10.0.0.1/v1/responses",
|
"http://10.0.0.1/v1/responses",
|
||||||
"ws://0.0.0.0:8080/v1/responses",
|
"ws://0.0.0.0:8080/v1/responses",
|
||||||
"ws://[::ffff:127.0.0.1]:8080/v1/responses",
|
"ws://[::ffff:127.0.0.1]:8080/v1/responses",
|
||||||
"wss://example.test/v1/responses#secret",
|
"wss://example.test/v1/responses#secret",
|
||||||
|
"ws://example.test/v1/responses#secret",
|
||||||
|
"http://[email protected]/v1/responses",
|
||||||
|
"ws://[email protected]/v1/responses",
|
||||||
|
"ftp://example.test/v1/responses",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
websocket_upstream_url(rejected, "invalid").is_err(),
|
websocket_upstream_url(rejected, "invalid").is_err(),
|
||||||
|
|||||||
@@ -129,9 +129,6 @@ pub(crate) fn normalize_admin_base_url(base_url: &str) -> Result<String, String>
|
|||||||
if parsed.host_str().is_none() {
|
if parsed.host_str().is_none() {
|
||||||
return Err("base_url 必须包含有效主机".to_string());
|
return Err("base_url 必须包含有效主机".to_string());
|
||||||
}
|
}
|
||||||
if !aether_http::is_https_or_loopback_http_url(&parsed) {
|
|
||||||
return Err("base_url 必须使用 HTTPS;HTTP 仅允许字面量 loopback 主机".to_string());
|
|
||||||
}
|
|
||||||
if !parsed.username().is_empty() || parsed.password().is_some() {
|
if !parsed.username().is_empty() || parsed.password().is_some() {
|
||||||
return Err("base_url 不允许包含用户名或密码".to_string());
|
return Err("base_url 不允许包含用户名或密码".to_string());
|
||||||
}
|
}
|
||||||
@@ -154,16 +151,42 @@ mod normalize_admin_base_url_tests {
|
|||||||
"https://user:[email protected]/v1",
|
"https://user:[email protected]/v1",
|
||||||
"https://api.example.test/v1?key=secret",
|
"https://api.example.test/v1?key=secret",
|
||||||
"https://api.example.test/v1#secret",
|
"https://api.example.test/v1#secret",
|
||||||
"http://api.example.test/v1",
|
"http://user:password@api.example.test/v1",
|
||||||
"http://10.0.0.1/v1",
|
"http://api.example.test/v1?key=secret",
|
||||||
"http://[::ffff:127.0.0.1]/v1",
|
"http://api.example.test/v1#secret",
|
||||||
|
"ftp://api.example.test/v1",
|
||||||
|
"file:///v1",
|
||||||
|
"api.example.test/v1",
|
||||||
|
"",
|
||||||
"https://",
|
"https://",
|
||||||
|
"http://",
|
||||||
"https://api.example.test:invalid/v1",
|
"https://api.example.test:invalid/v1",
|
||||||
] {
|
] {
|
||||||
assert!(normalize_admin_base_url(value).is_err(), "accepted {value}");
|
assert!(normalize_admin_base_url(value).is_err(), "accepted {value}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn endpoint_base_url_accepts_remote_http_hosts() {
|
||||||
|
for (raw_url, expected) in [
|
||||||
|
(
|
||||||
|
" HTTP://API.EXAMPLE.TEST:8080/v1/ ",
|
||||||
|
"http://api.example.test:8080/v1",
|
||||||
|
),
|
||||||
|
("http://8.8.8.8:8080/v1/", "http://8.8.8.8:8080/v1"),
|
||||||
|
("http://10.0.0.1:8080/v1/", "http://10.0.0.1:8080/v1"),
|
||||||
|
(
|
||||||
|
"http://[2606:4700:4700::1111]:8080/v1/",
|
||||||
|
"http://[2606:4700:4700::1111]:8080/v1",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_admin_base_url(raw_url).expect("HTTP base URL should be accepted"),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn endpoint_base_url_is_parsed_and_normalized() {
|
fn endpoint_base_url_is_parsed_and_normalized() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ async fn resolve_test_connection_target(
|
|||||||
{
|
{
|
||||||
return Err("provider endpoint must be an HTTP(S) URL without credentials or fragment");
|
return Err("provider endpoint must be an HTTP(S) URL without credentials or fragment");
|
||||||
}
|
}
|
||||||
if url.scheme() == "http" && !(allow_private_targets && literal_loopback) {
|
|
||||||
return Err("provider endpoint must use HTTPS");
|
|
||||||
}
|
|
||||||
let host = url
|
let host = url
|
||||||
.host_str()
|
.host_str()
|
||||||
.ok_or("provider endpoint is missing a host")?
|
.ok_or("provider endpoint is missing a host")?
|
||||||
@@ -574,10 +571,11 @@ mod tests {
|
|||||||
async fn test_connection_target_rejects_private_addresses_in_production_mode() {
|
async fn test_connection_target_rejects_private_addresses_in_production_mode() {
|
||||||
for raw_url in [
|
for raw_url in [
|
||||||
"http://127.0.0.1:8080/v1/chat/completions",
|
"http://127.0.0.1:8080/v1/chat/completions",
|
||||||
|
"http://10.0.0.1/v1/chat/completions",
|
||||||
|
"http://169.254.169.254/v1/chat/completions",
|
||||||
"https://10.0.0.1/v1/chat/completions",
|
"https://10.0.0.1/v1/chat/completions",
|
||||||
"https://[::1]/v1/chat/completions",
|
"https://[::1]/v1/chat/completions",
|
||||||
"https://localhost/v1/chat/completions",
|
"https://localhost/v1/chat/completions",
|
||||||
"http://8.8.8.8/v1/chat/completions",
|
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
resolve_test_connection_target(raw_url, false)
|
resolve_test_connection_target(raw_url, false)
|
||||||
@@ -588,6 +586,26 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_connection_target_accepts_public_http_and_https_addresses() {
|
||||||
|
for allow_private_targets in [false, true] {
|
||||||
|
for (raw_url, expected_port) in [
|
||||||
|
("http://8.8.8.8/v1/chat", 80),
|
||||||
|
("http://8.8.8.8:8080/v1/chat", 8080),
|
||||||
|
("https://8.8.8.8/v1/chat", 443),
|
||||||
|
] {
|
||||||
|
let target = resolve_test_connection_target(raw_url, allow_private_targets)
|
||||||
|
.await
|
||||||
|
.expect("public HTTP(S) provider target should resolve");
|
||||||
|
assert_eq!(target.url.as_str(), raw_url);
|
||||||
|
assert_eq!(target.host, "8.8.8.8");
|
||||||
|
assert_eq!(target.addresses.len(), 1);
|
||||||
|
assert_eq!(target.addresses[0].ip().to_string(), "8.8.8.8");
|
||||||
|
assert_eq!(target.addresses[0].port(), expected_port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_connection_target_allows_loopback_only_for_test_fixtures() {
|
async fn test_connection_target_allows_loopback_only_for_test_fixtures() {
|
||||||
let target = resolve_test_connection_target("http://127.0.0.1:8080/v1/chat", true)
|
let target = resolve_test_connection_target("http://127.0.0.1:8080/v1/chat", true)
|
||||||
@@ -596,10 +614,10 @@ mod tests {
|
|||||||
assert_eq!(target.host, "127.0.0.1");
|
assert_eq!(target.host, "127.0.0.1");
|
||||||
assert_eq!(target.addresses.len(), 1);
|
assert_eq!(target.addresses.len(), 1);
|
||||||
assert!(
|
assert!(
|
||||||
resolve_test_connection_target("http://8.8.8.8/v1/chat", true)
|
resolve_test_connection_target("http://10.0.0.1/v1/chat", true)
|
||||||
.await
|
.await
|
||||||
.is_err(),
|
.is_err(),
|
||||||
"test mode must not make cleartext public endpoints acceptable"
|
"test mode must not make private non-loopback HTTP endpoints acceptable"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
resolve_test_connection_target("https://10.0.0.1/v1/chat", true)
|
resolve_test_connection_target("https://10.0.0.1/v1/chat", true)
|
||||||
@@ -620,6 +638,9 @@ mod tests {
|
|||||||
for raw_url in [
|
for raw_url in [
|
||||||
"https://user:[email protected]/v1/chat",
|
"https://user:[email protected]/v1/chat",
|
||||||
"https://example.com/v1/chat#fragment",
|
"https://example.com/v1/chat#fragment",
|
||||||
|
"http://user:[email protected]/v1/chat",
|
||||||
|
"http://example.com/v1/chat#fragment",
|
||||||
|
"ftp://example.com/v1/chat",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
resolve_test_connection_target(raw_url, false)
|
resolve_test_connection_target(raw_url, false)
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ async fn gateway_returns_service_unavailable_for_admin_provider_endpoint_create_
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_creates_admin_provider_endpoint_locally_with_trusted_admin_principal() {
|
async fn gateway_creates_admin_http_provider_endpoint_locally_with_trusted_admin_principal() {
|
||||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||||
let upstream_hits_clone = Arc::clone(&upstream_hits);
|
let upstream_hits_clone = Arc::clone(&upstream_hits);
|
||||||
let upstream = Router::new().route(
|
let upstream = Router::new().route(
|
||||||
@@ -522,7 +522,7 @@ async fn gateway_creates_admin_provider_endpoint_locally_with_trusted_admin_prin
|
|||||||
.json(&json!({
|
.json(&json!({
|
||||||
"provider_id": "provider-openai",
|
"provider_id": "provider-openai",
|
||||||
"api_format": "openai:chat",
|
"api_format": "openai:chat",
|
||||||
"base_url": "https://api.openai.example/",
|
"base_url": "http://api.openai.example:8080/",
|
||||||
"custom_path": "/v1/chat/completions",
|
"custom_path": "/v1/chat/completions",
|
||||||
"max_retries": 5,
|
"max_retries": 5,
|
||||||
"config": {"foo": "bar"},
|
"config": {"foo": "bar"},
|
||||||
@@ -537,7 +537,7 @@ async fn gateway_creates_admin_provider_endpoint_locally_with_trusted_admin_prin
|
|||||||
assert_eq!(payload["provider_id"], "provider-openai");
|
assert_eq!(payload["provider_id"], "provider-openai");
|
||||||
assert_eq!(payload["provider_name"], "openai");
|
assert_eq!(payload["provider_name"], "openai");
|
||||||
assert_eq!(payload["api_format"], "openai:chat");
|
assert_eq!(payload["api_format"], "openai:chat");
|
||||||
assert_eq!(payload["base_url"], "https://api.openai.example");
|
assert_eq!(payload["base_url"], "http://api.openai.example:8080");
|
||||||
assert_eq!(payload["custom_path"], "/v1/chat/completions");
|
assert_eq!(payload["custom_path"], "/v1/chat/completions");
|
||||||
assert_eq!(payload["max_retries"], 5);
|
assert_eq!(payload["max_retries"], 5);
|
||||||
assert_eq!(payload["total_keys"], 0);
|
assert_eq!(payload["total_keys"], 0);
|
||||||
@@ -553,7 +553,7 @@ async fn gateway_creates_admin_provider_endpoint_locally_with_trusted_admin_prin
|
|||||||
assert_eq!(endpoints.len(), 1);
|
assert_eq!(endpoints.len(), 1);
|
||||||
assert_eq!(endpoints[0].provider_id, "provider-openai");
|
assert_eq!(endpoints[0].provider_id, "provider-openai");
|
||||||
assert_eq!(endpoints[0].api_format, "openai:chat");
|
assert_eq!(endpoints[0].api_format, "openai:chat");
|
||||||
assert_eq!(endpoints[0].base_url, "https://api.openai.example");
|
assert_eq!(endpoints[0].base_url, "http://api.openai.example:8080");
|
||||||
assert_eq!(endpoints[0].max_retries, Some(5));
|
assert_eq!(endpoints[0].max_retries, Some(5));
|
||||||
|
|
||||||
gateway_handle.abort();
|
gateway_handle.abort();
|
||||||
@@ -658,7 +658,7 @@ async fn gateway_rejects_streaming_policy_for_search_endpoint_before_catalog_wri
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_updates_admin_provider_endpoint_locally_with_trusted_admin_principal() {
|
async fn gateway_updates_admin_http_provider_endpoint_locally_with_trusted_admin_principal() {
|
||||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||||
let upstream_hits_clone = Arc::clone(&upstream_hits);
|
let upstream_hits_clone = Arc::clone(&upstream_hits);
|
||||||
let upstream = Router::new().route(
|
let upstream = Router::new().route(
|
||||||
@@ -720,7 +720,7 @@ async fn gateway_updates_admin_provider_endpoint_locally_with_trusted_admin_prin
|
|||||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
"base_url": "https://updated.openai.example/",
|
"base_url": "http://updated.openai.example:8080/",
|
||||||
"custom_path": "/v1/responses",
|
"custom_path": "/v1/responses",
|
||||||
"max_retries": 5,
|
"max_retries": 5,
|
||||||
"is_active": false,
|
"is_active": false,
|
||||||
@@ -736,7 +736,7 @@ async fn gateway_updates_admin_provider_endpoint_locally_with_trusted_admin_prin
|
|||||||
assert_eq!(payload["id"], "endpoint-openai-chat");
|
assert_eq!(payload["id"], "endpoint-openai-chat");
|
||||||
assert_eq!(payload["provider_id"], "provider-openai");
|
assert_eq!(payload["provider_id"], "provider-openai");
|
||||||
assert_eq!(payload["api_format"], "openai:chat");
|
assert_eq!(payload["api_format"], "openai:chat");
|
||||||
assert_eq!(payload["base_url"], "https://updated.openai.example");
|
assert_eq!(payload["base_url"], "http://updated.openai.example:8080");
|
||||||
assert_eq!(payload["custom_path"], "/v1/responses");
|
assert_eq!(payload["custom_path"], "/v1/responses");
|
||||||
assert_eq!(payload["max_retries"], 5);
|
assert_eq!(payload["max_retries"], 5);
|
||||||
assert_eq!(payload["is_active"], false);
|
assert_eq!(payload["is_active"], false);
|
||||||
@@ -751,7 +751,7 @@ async fn gateway_updates_admin_provider_endpoint_locally_with_trusted_admin_prin
|
|||||||
.await
|
.await
|
||||||
.expect("endpoints should read");
|
.expect("endpoints should read");
|
||||||
assert_eq!(endpoints.len(), 1);
|
assert_eq!(endpoints.len(), 1);
|
||||||
assert_eq!(endpoints[0].base_url, "https://updated.openai.example");
|
assert_eq!(endpoints[0].base_url, "http://updated.openai.example:8080");
|
||||||
assert_eq!(endpoints[0].custom_path.as_deref(), Some("/v1/responses"));
|
assert_eq!(endpoints[0].custom_path.as_deref(), Some("/v1/responses"));
|
||||||
assert_eq!(endpoints[0].max_retries, Some(5));
|
assert_eq!(endpoints[0].max_retries, Some(5));
|
||||||
assert!(!endpoints[0].is_active);
|
assert!(!endpoints[0].is_active);
|
||||||
|
|||||||
Reference in New Issue
Block a user