mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Send model routing hints to the Codex backend (#37345)
## What changed - Add an `x-codex-routing-hint` header containing the request model and, when selected, the service tier. - Send the hint on Responses HTTP requests, remote compaction requests, and WebSocket handshakes, including prewarm connections. - Limit the header to Codex-backend authentication with the OpenAI provider, omitting it for API-key and provider-specific credential paths. ## Testing - Cover routing hints for HTTP, remote compaction, WebSocket streaming, and tiered prewarming, along with the credential paths where the header must be absent. GitOrigin-RevId: d8d415d7fadddecf9a132d05920b856a35ac9960
This commit is contained in:
@@ -141,6 +141,7 @@ use codex_response_debug_context::telemetry_transport_error_message;
|
||||
|
||||
pub const OPENAI_BETA_HEADER: &str = "OpenAI-Beta";
|
||||
pub const X_CODEX_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id";
|
||||
pub const X_CODEX_ROUTING_HINT_HEADER: &str = "x-codex-routing-hint";
|
||||
pub const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state";
|
||||
pub const X_CODEX_TURN_METADATA_HEADER: &str = "x-codex-turn-metadata";
|
||||
pub const X_CODEX_PARENT_THREAD_ID_HEADER: &str = "x-codex-parent-thread-id";
|
||||
@@ -626,6 +627,13 @@ impl ModelClient {
|
||||
if let Some(header_value) = self.generate_attestation_header_for().await {
|
||||
extra_headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
|
||||
}
|
||||
if let Some(header_value) = self.build_routing_hint_header(
|
||||
client_setup.auth.as_ref(),
|
||||
&model,
|
||||
service_tier.as_deref(),
|
||||
) {
|
||||
extra_headers.insert(X_CODEX_ROUTING_HINT_HEADER, header_value);
|
||||
}
|
||||
add_responses_lite_header(&mut extra_headers, model_info.use_responses_lite);
|
||||
let compact_request_timeout = client_setup
|
||||
.api_provider
|
||||
@@ -980,6 +988,31 @@ impl ModelClient {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_routing_hint_header(
|
||||
&self,
|
||||
auth: Option<&CodexAuth>,
|
||||
model: &str,
|
||||
service_tier: Option<&str>,
|
||||
) -> Option<HeaderValue> {
|
||||
let provider = self.state.provider.info();
|
||||
if !auth.is_some_and(CodexAuth::uses_codex_backend)
|
||||
|| !provider.is_openai()
|
||||
|| !provider.requires_openai_auth
|
||||
|| provider.env_key.is_some()
|
||||
|| provider.experimental_bearer_token.is_some()
|
||||
|| provider.auth.is_some()
|
||||
|| provider.aws.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let routing_hint = match service_tier {
|
||||
Some(tier) => format!("model={model};tier={tier}"),
|
||||
None => format!("model={model}"),
|
||||
};
|
||||
HeaderValue::from_str(&routing_hint).ok()
|
||||
}
|
||||
|
||||
fn build_api_transport(
|
||||
&self,
|
||||
api_provider: &ApiProvider,
|
||||
@@ -1106,6 +1139,9 @@ impl ModelClient {
|
||||
Some(responses_metadata.thread_id.to_string()),
|
||||
));
|
||||
headers.extend(self.build_responses_compatibility_headers(responses_metadata));
|
||||
if let Some(routing_hint) = &responses_metadata.routing_hint {
|
||||
headers.insert(X_CODEX_ROUTING_HINT_HEADER, routing_hint.clone());
|
||||
}
|
||||
if let Some(header_value) = self.generate_attestation_header_for().await {
|
||||
headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
|
||||
}
|
||||
@@ -1454,6 +1490,15 @@ impl ModelClientSession {
|
||||
service_tier.clone(),
|
||||
responses_metadata,
|
||||
)?;
|
||||
if let Some(header_value) = self.client.build_routing_hint_header(
|
||||
client_setup.auth.as_ref(),
|
||||
&request.model,
|
||||
request.service_tier.as_deref(),
|
||||
) {
|
||||
options
|
||||
.extra_headers
|
||||
.insert(X_CODEX_ROUTING_HINT_HEADER, header_value);
|
||||
}
|
||||
self.client
|
||||
.prepare_response_items_for_request(&mut request.input);
|
||||
let request_session_telemetry =
|
||||
@@ -1566,6 +1611,12 @@ impl ModelClientSession {
|
||||
service_tier.clone(),
|
||||
responses_metadata,
|
||||
)?;
|
||||
let mut websocket_metadata = responses_metadata.clone();
|
||||
websocket_metadata.routing_hint = self.client.build_routing_hint_header(
|
||||
client_setup.auth.as_ref(),
|
||||
&request.model,
|
||||
request.service_tier.as_deref(),
|
||||
);
|
||||
let request_session_telemetry = if warmup {
|
||||
// `generate=false` prewarm is connection setup, not an inference request.
|
||||
session_telemetry.clone()
|
||||
@@ -1583,7 +1634,7 @@ impl ModelClientSession {
|
||||
session_telemetry,
|
||||
api_provider: client_setup.api_provider,
|
||||
api_auth: client_setup.api_auth,
|
||||
responses_metadata,
|
||||
responses_metadata: &websocket_metadata,
|
||||
auth_context: request_auth_context,
|
||||
request_route_telemetry: RequestRouteTelemetry::for_endpoint(
|
||||
RESPONSES_ENDPOINT,
|
||||
|
||||
@@ -184,6 +184,7 @@ pub use attestation::GenerateAttestationFuture;
|
||||
pub use client::ModelClient;
|
||||
pub use client::ModelClientSession;
|
||||
pub use client::X_CODEX_INSTALLATION_ID_HEADER;
|
||||
pub use client::X_CODEX_ROUTING_HINT_HEADER;
|
||||
pub use client::X_CODEX_TURN_METADATA_HEADER;
|
||||
pub use client_common::Prompt;
|
||||
pub use client_common::ResponseEvent;
|
||||
|
||||
@@ -161,6 +161,7 @@ pub struct CodexResponsesMetadata {
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) turn_id: Option<String>,
|
||||
pub(crate) routing_hint: Option<HeaderValue>,
|
||||
pub(crate) window_id: String,
|
||||
pub(crate) request_kind: Option<CodexResponsesRequestKind>,
|
||||
pub(crate) forked_from_thread_id: Option<ThreadId>,
|
||||
@@ -188,6 +189,7 @@ impl CodexResponsesMetadata {
|
||||
session_id,
|
||||
thread_id,
|
||||
turn_id: None,
|
||||
routing_hint: None,
|
||||
window_id,
|
||||
request_kind: None,
|
||||
forked_from_thread_id: None,
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_core::Prompt;
|
||||
use codex_core::ResponseEvent;
|
||||
use codex_core::StartThreadOptions;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::X_CODEX_ROUTING_HINT_HEADER;
|
||||
use codex_core::resolve_installation_id;
|
||||
use codex_core::thread_store_from_config;
|
||||
use codex_extension_api::empty_extension_registry;
|
||||
@@ -1326,6 +1327,7 @@ async fn includes_session_id_thread_id_and_model_headers_in_request() {
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
assert_eq!(request.path(), "/v1/responses");
|
||||
assert_eq!(request.header(X_CODEX_ROUTING_HINT_HEADER), None);
|
||||
let request_session_id = request.header("session-id").expect("session-id header");
|
||||
let request_thread_id = request.header("thread-id").expect("thread-id header");
|
||||
let request_authorization = request
|
||||
@@ -1656,6 +1658,13 @@ async fn chatgpt_auth_sends_correct_request() {
|
||||
.header("chatgpt-account-id")
|
||||
.expect("chatgpt-account-id header");
|
||||
let request_body = request.body_json();
|
||||
let model = request_body["model"]
|
||||
.as_str()
|
||||
.expect("missing request model");
|
||||
assert_eq!(
|
||||
request.header(X_CODEX_ROUTING_HINT_HEADER),
|
||||
Some(format!("model={model}"))
|
||||
);
|
||||
|
||||
let request_session_id = request.header("session-id").expect("session-id header");
|
||||
let request_thread_id = request.header("thread-id").expect("thread-id header");
|
||||
@@ -2525,6 +2534,7 @@ async fn sequential_cutoff_is_omitted_for_non_openai_provider() -> anyhow::Resul
|
||||
)
|
||||
.await;
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.model_provider.name = "mock".to_string();
|
||||
let _ = config
|
||||
@@ -2550,7 +2560,9 @@ async fn sequential_cutoff_is_omitted_for_non_openai_provider() -> anyhow::Resul
|
||||
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let request_body = resp_mock.single_request().body_json();
|
||||
let request = resp_mock.single_request();
|
||||
assert_eq!(request.header(X_CODEX_ROUTING_HINT_HEADER), None);
|
||||
let request_body = request.body_json();
|
||||
pretty_assertions::assert_eq!(request_body.get("stream_options"), None);
|
||||
|
||||
Ok(())
|
||||
@@ -3766,7 +3778,7 @@ async fn env_var_overrides_loaded_auth() {
|
||||
.await;
|
||||
|
||||
let provider = ModelProviderInfo {
|
||||
name: "custom".to_string(),
|
||||
name: ModelProviderInfo::create_openai_provider(/*base_url*/ None).name,
|
||||
base_url: Some(format!("{}/openai", server.uri())),
|
||||
// Reuse the existing environment variable to avoid using unsafe code
|
||||
env_key: Some(EXISTING_ENV_VAR_WITH_NON_EMPTY_VALUE.to_string()),
|
||||
@@ -3820,6 +3832,15 @@ async fn env_var_overrides_loaded_auth() {
|
||||
.unwrap();
|
||||
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let request = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("read recorded requests")
|
||||
.into_iter()
|
||||
.find(|request| request.url.path() == "/openai/responses")
|
||||
.expect("missing provider request");
|
||||
assert_eq!(request.headers.get(X_CODEX_ROUTING_HINT_HEADER), None);
|
||||
}
|
||||
|
||||
fn create_dummy_codex_auth() -> CodexAuth {
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_core::ModelClient;
|
||||
use codex_core::ModelClientSession;
|
||||
use codex_core::Prompt;
|
||||
use codex_core::ResponseEvent;
|
||||
use codex_core::X_CODEX_ROUTING_HINT_HEADER;
|
||||
use codex_core::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER;
|
||||
use codex_core::test_support::with_parent_turn;
|
||||
use codex_features::Feature;
|
||||
@@ -29,6 +30,7 @@ use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelServiceTier;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
@@ -160,7 +162,7 @@ async fn responses_websocket_streams_request() {
|
||||
]]])
|
||||
.await;
|
||||
|
||||
let harness = websocket_harness(&server).await;
|
||||
let harness = websocket_harness_for_codex_backend(&server).await;
|
||||
let mut client_session = harness.client.new_session();
|
||||
let prompt = prompt_with_input(vec![message_item("hello")]);
|
||||
|
||||
@@ -179,6 +181,10 @@ async fn responses_websocket_streams_request() {
|
||||
handshake.header(OPENAI_BETA_HEADER),
|
||||
Some(WS_V2_BETA_HEADER_VALUE.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
handshake.header(X_CODEX_ROUTING_HINT_HEADER),
|
||||
Some(format!("model={MODEL}"))
|
||||
);
|
||||
assert_eq!(
|
||||
handshake.header(X_CLIENT_REQUEST_ID_HEADER),
|
||||
Some(harness.thread_id.to_string())
|
||||
@@ -210,6 +216,42 @@ async fn responses_websocket_streams_request() {
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_websocket_omits_routing_hint_for_provider_with_own_credentials() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = start_websocket_server(vec![vec![vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_completed("resp-1"),
|
||||
]]])
|
||||
.await;
|
||||
|
||||
let mut provider = websocket_provider(&server);
|
||||
provider.name = ModelProviderInfo::create_openai_provider(/*base_url*/ None).name;
|
||||
provider.experimental_bearer_token = Some("provider-specific-token".to_string());
|
||||
let harness = websocket_harness_with_provider_options_and_auth(
|
||||
provider,
|
||||
/*runtime_metrics_enabled*/ false,
|
||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||
/*enabled_features*/ &[],
|
||||
Some(CodexAuth::create_dummy_chatgpt_auth_for_testing()),
|
||||
)
|
||||
.await;
|
||||
let mut client_session = harness.client.new_session();
|
||||
let prompt = prompt_with_input(vec![message_item("hello")]);
|
||||
|
||||
stream_until_complete(&mut client_session, &harness, &prompt).await;
|
||||
|
||||
let handshake = server.single_handshake();
|
||||
assert_eq!(
|
||||
handshake.header("authorization"),
|
||||
Some("Bearer provider-specific-token".to_string())
|
||||
);
|
||||
assert_eq!(handshake.header(X_CODEX_ROUTING_HINT_HEADER), None);
|
||||
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_websocket_omits_unprefixed_item_ids_without_mutating_prompt() {
|
||||
skip_if_no_network!();
|
||||
@@ -913,6 +955,59 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes(
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_websocket_prewarm_includes_model_and_tier_routing_hint() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = start_websocket_server(vec![vec![vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_completed("resp-1"),
|
||||
]]])
|
||||
.await;
|
||||
|
||||
let harness = websocket_harness_for_codex_backend(&server).await;
|
||||
let mut model_info = harness.model_info.clone();
|
||||
let service_tier = ServiceTier::Fast.request_value();
|
||||
model_info.service_tiers.push(ModelServiceTier {
|
||||
id: service_tier.to_string(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Priority processing".to_string(),
|
||||
});
|
||||
let mut client_session = harness.client.new_session();
|
||||
let prompt = prompt_with_input(vec![message_item("hello")]);
|
||||
let responses_metadata = prewarm_metadata(&harness, /*turn_id*/ None);
|
||||
client_session
|
||||
.prewarm_websocket(
|
||||
&prompt,
|
||||
&model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
Some(service_tier.to_string()),
|
||||
&responses_metadata,
|
||||
)
|
||||
.await
|
||||
.expect("websocket prewarm failed");
|
||||
|
||||
let handshake = server.single_handshake();
|
||||
assert_eq!(
|
||||
handshake.header(X_CODEX_ROUTING_HINT_HEADER),
|
||||
Some(format!("model={MODEL};tier={service_tier}"))
|
||||
);
|
||||
assert_eq!(
|
||||
handshake.header(OPENAI_BETA_HEADER),
|
||||
Some(WS_V2_BETA_HEADER_VALUE.to_string())
|
||||
);
|
||||
let connection = server.single_connection();
|
||||
let prewarm = connection
|
||||
.first()
|
||||
.expect("missing prewarm request")
|
||||
.body_json();
|
||||
assert_eq!(prewarm["service_tier"].as_str(), Some(service_tier));
|
||||
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_websocket_prewarm_uses_v2_when_provider_supports_websockets() {
|
||||
skip_if_no_network!();
|
||||
@@ -995,6 +1090,12 @@ async fn responses_websocket_preconnect_runs_when_only_v2_feature_enabled() {
|
||||
server.single_handshake().header("x-codex-turn-metadata"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
server
|
||||
.single_handshake()
|
||||
.header(X_CODEX_ROUTING_HINT_HEADER),
|
||||
None
|
||||
);
|
||||
|
||||
let prompt = prompt_with_input(vec![message_item("hello")]);
|
||||
stream_until_complete(&mut client_session, &harness, &prompt).await;
|
||||
@@ -2300,6 +2401,18 @@ async fn websocket_harness(server: &WebSocketTestServer) -> WebsocketTestHarness
|
||||
websocket_harness_with_runtime_metrics(server, /*runtime_metrics_enabled*/ false).await
|
||||
}
|
||||
|
||||
async fn websocket_harness_for_codex_backend(server: &WebSocketTestServer) -> WebsocketTestHarness {
|
||||
let provider = ModelProviderInfo::create_openai_provider(Some(format!("{}/v1", server.uri())));
|
||||
websocket_harness_with_provider_options_and_auth(
|
||||
provider,
|
||||
/*runtime_metrics_enabled*/ false,
|
||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||
/*enabled_features*/ &[],
|
||||
Some(CodexAuth::create_dummy_chatgpt_auth_for_testing()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn websocket_harness_with_runtime_metrics(
|
||||
server: &WebSocketTestServer,
|
||||
runtime_metrics_enabled: bool,
|
||||
@@ -2332,6 +2445,23 @@ async fn websocket_harness_with_provider_options(
|
||||
runtime_metrics_enabled: bool,
|
||||
concurrent_reasoning_summaries_enabled: bool,
|
||||
enabled_features: &[Feature],
|
||||
) -> WebsocketTestHarness {
|
||||
websocket_harness_with_provider_options_and_auth(
|
||||
provider,
|
||||
runtime_metrics_enabled,
|
||||
concurrent_reasoning_summaries_enabled,
|
||||
enabled_features,
|
||||
/*auth*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn websocket_harness_with_provider_options_and_auth(
|
||||
provider: ModelProviderInfo,
|
||||
runtime_metrics_enabled: bool,
|
||||
concurrent_reasoning_summaries_enabled: bool,
|
||||
enabled_features: &[Feature],
|
||||
auth: Option<CodexAuth>,
|
||||
) -> WebsocketTestHarness {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
@@ -2361,8 +2491,10 @@ async fn websocket_harness_with_provider_options(
|
||||
let model_info = codex_core::test_support::construct_model_info_offline(MODEL, &config);
|
||||
let thread_id = ThreadId::new();
|
||||
let session_id = SessionId::new();
|
||||
let auth_manager =
|
||||
codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("Test API Key"));
|
||||
let client_auth_manager = auth.map(codex_core::test_support::auth_manager_from_auth);
|
||||
let auth_manager = client_auth_manager.clone().unwrap_or_else(|| {
|
||||
codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
});
|
||||
let exporter = InMemoryMetricExporter::default();
|
||||
let metrics = MetricsClient::new(
|
||||
MetricsConfig::in_memory("test", "codex-core", env!("CARGO_PKG_VERSION"), exporter)
|
||||
@@ -2385,7 +2517,7 @@ async fn websocket_harness_with_provider_options(
|
||||
let effort = None;
|
||||
let summary = ReasoningSummary::Auto;
|
||||
let client = ModelClient::new(
|
||||
/*auth_manager*/ None,
|
||||
client_auth_manager,
|
||||
AgentIdentityAuthPolicy::JwtOnly,
|
||||
thread_id,
|
||||
provider.clone(),
|
||||
|
||||
@@ -5,6 +5,7 @@ use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_core::StartThreadOptions;
|
||||
use codex_core::X_CODEX_ROUTING_HINT_HEADER;
|
||||
use codex_core::compact::SUMMARY_PREFIX;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -790,6 +791,14 @@ async fn remote_compact_uses_agent_identity_assertion() -> Result<()> {
|
||||
compact_request.header("chatgpt-account-id").as_deref(),
|
||||
Some("account-compact")
|
||||
);
|
||||
let compact_body = compact_request.body_json();
|
||||
let model = compact_body["model"]
|
||||
.as_str()
|
||||
.expect("missing request model");
|
||||
assert_eq!(
|
||||
compact_request.header(X_CODEX_ROUTING_HINT_HEADER),
|
||||
Some(format!("model={model}"))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -801,6 +810,7 @@ async fn assert_remote_manual_compact_request_parity(
|
||||
snapshot_name: &str,
|
||||
scenario: &str,
|
||||
) -> Result<()> {
|
||||
let uses_codex_backend = auth.uses_codex_backend();
|
||||
let mut builder = test_codex().with_auth(auth);
|
||||
if let Some(service_tier) = configured_service_tier {
|
||||
builder = builder.with_config(move |config| {
|
||||
@@ -969,6 +979,25 @@ async fn assert_remote_manual_compact_request_parity(
|
||||
let compact_request = compact_mock.single_request();
|
||||
let normal_body = normal_request.body_json();
|
||||
let compact_body = compact_request.body_json();
|
||||
let expected_routing_hint = |body: &Value| {
|
||||
if !uses_codex_backend {
|
||||
return None;
|
||||
}
|
||||
|
||||
let model = body["model"].as_str().expect("missing request model");
|
||||
match body.get("service_tier").and_then(Value::as_str) {
|
||||
Some(tier) => Some(format!("model={model};tier={tier}")),
|
||||
None => Some(format!("model={model}")),
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
normal_request.header(X_CODEX_ROUTING_HINT_HEADER),
|
||||
expected_routing_hint(&normal_body)
|
||||
);
|
||||
assert_eq!(
|
||||
compact_request.header(X_CODEX_ROUTING_HINT_HEADER),
|
||||
expected_routing_hint(&compact_body)
|
||||
);
|
||||
|
||||
let mut expected_compact_body_without_input = normal_body.clone();
|
||||
let expected_compact_object = expected_compact_body_without_input
|
||||
|
||||
Reference in New Issue
Block a user