Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 154 additions & 1 deletion code-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ use code_otel::otel_event_manager::OtelEventManager;
use std::sync::Arc;
use std::sync::Mutex;

const RESPONSES_BETA_HEADER_V1: &str = "responses=v1";
const RESPONSES_BETA_HEADER_EXPERIMENTAL: &str = "responses=experimental";

#[derive(Debug, Deserialize)]
struct ErrorResponse {
error: Error,
Expand Down Expand Up @@ -494,12 +497,25 @@ impl ModelClient {
.create_request_builder(&self.client, &auth)
.await?;

let has_beta_header = req_builder
.try_clone()
.and_then(|builder| builder.build().ok())
.map_or(false, |req| req.headers().contains_key("OpenAI-Beta"));

if !has_beta_header {
let beta_value = if self.provider.is_public_openai_responses_endpoint() {
RESPONSES_BETA_HEADER_V1
} else {
RESPONSES_BETA_HEADER_EXPERIMENTAL
};
req_builder = req_builder.header("OpenAI-Beta", beta_value);
}

// `Codex-Task-Type` differentiates traffic for caching; default to "standard" until
// task-specific dispatch is re-introduced.
let codex_task_type = "standard";

req_builder = req_builder
.header("OpenAI-Beta", "responses=experimental")
// Send `conversation_id`/`session_id` so the server can hit the prompt-cache.
.header("conversation_id", session_id_str.clone())
.header("session_id", session_id_str.clone())
Expand Down Expand Up @@ -1360,6 +1376,8 @@ async fn stream_from_fixture(
#[cfg(test)]
mod tests {
use super::*;
use crate::model_provider_info::{ModelProviderInfo, WireApi};
use std::collections::HashMap;
use serde_json::json;
use tokio::sync::mpsc;
use tokio_test::io::Builder as IoBuilder;
Expand All @@ -1369,6 +1387,141 @@ mod tests {
// Helpers
// ────────────────────────────

#[tokio::test]
async fn responses_request_uses_beta_header_for_public_openai() {
let provider = ModelProviderInfo {
name: "openai".to_string(),
base_url: Some("https://api.openai.com/v1".to_string()),
env_key: None,
env_key_instructions: None,
wire_api: WireApi::Responses,
query_params: None,
http_headers: None,
env_http_headers: None,
request_max_retries: Some(0),
stream_max_retries: None,
stream_idle_timeout_ms: None,
requires_openai_auth: false,
openrouter: None,
};

let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("client");

let mut builder = provider
.create_request_builder(&client, &None)
.await
.expect("builder");
let has_beta = builder
.try_clone()
.and_then(|b| b.build().ok())
.map_or(false, |req| req.headers().contains_key("OpenAI-Beta"));
if !has_beta {
builder = builder.header("OpenAI-Beta", RESPONSES_BETA_HEADER_V1);
}
let request = builder
.try_clone()
.expect("clone request builder")
.build()
.expect("build request");

let header_value = request
.headers()
.get("OpenAI-Beta")
.expect("OpenAI-Beta header present");
assert_eq!(header_value, RESPONSES_BETA_HEADER_V1);
}

#[tokio::test]
async fn responses_request_uses_experimental_for_backend() {
let provider = ModelProviderInfo {
name: "backend".to_string(),
base_url: Some("https://chatgpt.com/backend-api/codex".to_string()),
env_key: None,
env_key_instructions: None,
wire_api: WireApi::Responses,
query_params: None,
http_headers: None,
env_http_headers: None,
request_max_retries: Some(0),
stream_max_retries: None,
stream_idle_timeout_ms: None,
requires_openai_auth: false,
openrouter: None,
};

let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("client");

let mut builder = provider
.create_request_builder(&client, &None)
.await
.expect("builder");
let has_beta = builder
.try_clone()
.and_then(|b| b.build().ok())
.map_or(false, |req| req.headers().contains_key("OpenAI-Beta"));
if !has_beta {
builder = builder.header("OpenAI-Beta", RESPONSES_BETA_HEADER_EXPERIMENTAL);
}
let request = builder
.try_clone()
.expect("clone request builder")
.build()
.expect("build request");

let header_value = request
.headers()
.get("OpenAI-Beta")
.expect("OpenAI-Beta header present");
assert_eq!(header_value, RESPONSES_BETA_HEADER_EXPERIMENTAL);
}

#[tokio::test]
async fn responses_request_respects_preexisting_beta_header() {
let mut headers = HashMap::new();
headers.insert("OpenAI-Beta".to_string(), "custom".to_string());
let provider = ModelProviderInfo {
name: "custom".to_string(),
base_url: Some("https://api.openai.com/v1".to_string()),
env_key: None,
env_key_instructions: None,
wire_api: WireApi::Responses,
query_params: None,
http_headers: Some(headers),
env_http_headers: None,
request_max_retries: Some(0),
stream_max_retries: None,
stream_idle_timeout_ms: None,
requires_openai_auth: false,
openrouter: None,
};

let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("client");

let request = provider
.create_request_builder(&client, &None)
.await
.expect("builder")
.try_clone()
.expect("clone request builder")
.build()
.expect("build request");

let header_value = request
.headers()
.get("OpenAI-Beta")
.expect("OpenAI-Beta header present");
assert_eq!(header_value, "custom");
}

/// Runs the SSE parser on pre-chunked byte slices and returns every event
/// (including any final `Err` from a stream-closure check).
async fn collect_events(
Expand Down
31 changes: 30 additions & 1 deletion code-rs/core/src/model_provider_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ impl ModelProviderInfo {
})
}

pub(crate) fn get_full_url(&self, auth: &Option<CodexAuth>) -> String {
pub(crate) fn get_full_url(&self, auth: &Option<CodexAuth>) -> String {
let default_base_url = if matches!(
auth,
Some(CodexAuth {
Expand Down Expand Up @@ -291,6 +291,35 @@ impl ModelProviderInfo {
.unwrap_or(false)
}

pub(crate) fn is_backend_responses_endpoint(&self) -> bool {
if self.wire_api != WireApi::Responses {
return false;
}

if self.name.eq_ignore_ascii_case("backend") {
return true;
}

self.base_url
.as_ref()
.map_or(false, |base| base.contains("/backend-api"))
}

pub(crate) fn is_public_openai_responses_endpoint(&self) -> bool {
if self.wire_api != WireApi::Responses {
return false;
}
if self.is_backend_responses_endpoint() || self.is_azure_responses_endpoint() {
return false;
}

self.base_url
.as_ref()
.and_then(|base| url::Url::parse(base).ok())
.and_then(|parsed| parsed.host_str().map(|host| host.eq_ignore_ascii_case("api.openai.com")))
.unwrap_or(true)
}

/// Apply provider-specific HTTP headers (both static and environment-based)
/// onto an existing `reqwest::RequestBuilder` and return the updated
/// builder.
Expand Down
Loading
Loading