diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 997133d51..c60c4507d 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -433,36 +433,33 @@ impl ModelClient { RequestRouteTelemetry::for_endpoint(RESPONSES_COMPACT_ENDPOINT), self.state.auth_env_telemetry.clone(), ); + let request = self.build_responses_request( + &client_setup.api_provider, + prompt, + model_info, + effort, + summary, + /*service_tier*/ None, + )?; + let ResponsesApiRequest { + model, + instructions, + input, + tools, + parallel_tool_calls, + reasoning, + text, + .. + } = request; let client = ApiCompactClient::new(transport, client_setup.api_provider, client_setup.api_auth) .with_telemetry(Some(request_telemetry)); - - let instructions = prompt.base_instructions.text.clone(); - let input = prompt.get_formatted_input(); - let tools = create_tools_json_for_responses_api(&prompt.tools)?; - let reasoning = Self::build_reasoning(model_info, effort, summary); - let verbosity = if model_info.support_verbosity { - self.state.model_verbosity.or(model_info.default_verbosity) - } else { - if self.state.model_verbosity.is_some() { - warn!( - "model_verbosity is set but ignored as the model does not support verbosity: {}", - model_info.slug - ); - } - None - }; - let text = create_text_param_for_request( - verbosity, - &prompt.output_schema, - prompt.output_schema_strict, - ); let payload = ApiCompactionInput { - model: &model_info.slug, + model: &model, input: &input, instructions: &instructions, tools, - parallel_tool_calls: prompt.parallel_tool_calls, + parallel_tool_calls, reasoning, text, }; @@ -664,6 +661,67 @@ impl ModelClient { } } + fn build_responses_request( + &self, + provider: &codex_api::Provider, + prompt: &Prompt, + model_info: &ModelInfo, + effort: Option, + summary: ReasoningSummaryConfig, + service_tier: Option, + ) -> Result { + let instructions = &prompt.base_instructions.text; + let input = prompt.get_formatted_input(); + let tools = create_tools_json_for_responses_api(&prompt.tools)?; + let reasoning = Self::build_reasoning(model_info, effort, summary); + let include = if reasoning.is_some() { + vec!["reasoning.encrypted_content".to_string()] + } else { + Vec::new() + }; + let verbosity = if model_info.support_verbosity { + self.state.model_verbosity.or(model_info.default_verbosity) + } else { + if self.state.model_verbosity.is_some() { + warn!( + "model_verbosity is set but ignored as the model does not support verbosity: {}", + model_info.slug + ); + } + None + }; + let text = create_text_param_for_request( + verbosity, + &prompt.output_schema, + prompt.output_schema_strict, + ); + let prompt_cache_key = Some(self.state.conversation_id.to_string()); + let request = ResponsesApiRequest { + model: model_info.slug.clone(), + instructions: instructions.clone(), + input, + tools, + tool_choice: "auto".to_string(), + parallel_tool_calls: prompt.parallel_tool_calls, + reasoning, + store: provider.is_azure_responses_endpoint(), + stream: true, + include, + service_tier: match service_tier { + Some(ServiceTier::Fast) => Some("priority".to_string()), + Some(service_tier) => Some(service_tier.to_string()), + None => None, + }, + prompt_cache_key, + text, + client_metadata: Some(HashMap::from([( + X_CODEX_INSTALLATION_ID_HEADER.to_string(), + self.state.installation_id.clone(), + )])), + }; + Ok(request) + } + /// Returns whether the Responses-over-WebSocket transport is active for this session. /// /// WebSocket use is controlled by provider capability and session-scoped fallback state. @@ -833,82 +891,6 @@ impl ModelClientSession { .set_connection_reused(/*connection_reused*/ false); } - fn build_responses_request( - &self, - provider: &codex_api::Provider, - prompt: &Prompt, - model_info: &ModelInfo, - effort: Option, - summary: ReasoningSummaryConfig, - service_tier: Option, - ) -> Result { - let instructions = &prompt.base_instructions.text; - let input = prompt.get_formatted_input(); - let tools = create_tools_json_for_responses_api(&prompt.tools)?; - let default_reasoning_effort = model_info.default_reasoning_level; - let reasoning = if model_info.supports_reasoning_summaries { - Some(Reasoning { - effort: effort.or(default_reasoning_effort), - summary: if summary == ReasoningSummaryConfig::None { - None - } else { - Some(summary) - }, - }) - } else { - None - }; - let include = if reasoning.is_some() { - vec!["reasoning.encrypted_content".to_string()] - } else { - Vec::new() - }; - let verbosity = if model_info.support_verbosity { - self.client - .state - .model_verbosity - .or(model_info.default_verbosity) - } else { - if self.client.state.model_verbosity.is_some() { - warn!( - "model_verbosity is set but ignored as the model does not support verbosity: {}", - model_info.slug - ); - } - None - }; - let text = create_text_param_for_request( - verbosity, - &prompt.output_schema, - prompt.output_schema_strict, - ); - let prompt_cache_key = Some(self.client.state.conversation_id.to_string()); - let request = ResponsesApiRequest { - model: model_info.slug.clone(), - instructions: instructions.clone(), - input, - tools, - tool_choice: "auto".to_string(), - parallel_tool_calls: prompt.parallel_tool_calls, - reasoning, - store: provider.is_azure_responses_endpoint(), - stream: true, - include, - service_tier: match service_tier { - Some(ServiceTier::Fast) => Some("priority".to_string()), - Some(service_tier) => Some(service_tier.to_string()), - None => None, - }, - prompt_cache_key, - text, - client_metadata: Some(HashMap::from([( - X_CODEX_INSTALLATION_ID_HEADER.to_string(), - self.client.state.installation_id.clone(), - )])), - }; - Ok(request) - } - #[allow(clippy::too_many_arguments)] /// Builds shared Responses API transport options and request-body options. /// @@ -1209,7 +1191,7 @@ impl ModelClientSession { let compression = self.responses_request_compression(client_setup.auth.as_ref()); let options = self.build_responses_options(turn_metadata_header, compression); - let request = self.build_responses_request( + let request = self.client.build_responses_request( &client_setup.api_provider, prompt, model_info, @@ -1315,7 +1297,7 @@ impl ModelClientSession { let compression = self.responses_request_compression(client_setup.auth.as_ref()); let options = self.build_responses_options(turn_metadata_header, compression); - let request = self.build_responses_request( + let request = self.client.build_responses_request( &client_setup.api_provider, prompt, model_info, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index f4960af55..6829ea932 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -770,10 +770,15 @@ async fn includes_conversation_id_and_model_headers_in_request() { let installation_id = std::fs::read_to_string(test.codex_home_path().join(INSTALLATION_ID_FILENAME)) .expect("read installation id"); + let session_id_string = session_id.to_string(); - assert_eq!(request_session_id, session_id.to_string()); + assert_eq!(request_session_id, session_id_string); assert_eq!(request_originator, originator().value); assert_eq!(request_authorization, "Bearer Test API Key"); + assert_eq!( + request_body["prompt_cache_key"].as_str(), + Some(session_id_string.as_str()) + ); assert_eq!( request_body["client_metadata"]["x-codex-installation-id"].as_str(), Some(installation_id.as_str())