Skip to content
Merged
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
71 changes: 60 additions & 11 deletions codex-rs/app-server/tests/suite/v2/imagegen_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,14 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
.build()
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
start_image_generation_turn(&mut mcp).await?;
start_image_generation_turn(
&mut mcp,
ThreadStartParams {
service_name: Some("chatgpt_cca".to_string()),
..Default::default()
},
)
.await?;

let completed = timeout(
DEFAULT_READ_TIMEOUT,
Expand Down Expand Up @@ -123,6 +130,23 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
assert_eq!(result, RESULT);
assert_eq!(std::fs::read(&saved_path)?, TINY_PNG_BYTES);

let image_request = server
.received_requests()
.await
.context("failed to fetch received requests")?
.into_iter()
.find(|request| request.url.path() == "/api/codex/images/generations")
.context("image generation request should be sent")?;
assert_eq!(
image_request
.headers
.get("originator")
.context("standalone image generation should include the thread originator")?
.to_str()
.context("standalone image generation originator should be valid ASCII")?,
"chatgpt_cca"
);

let requests = response_mock.requests();
assert_eq!(requests.len(), 2);
let output = requests[1].function_call_output(call_id);
Expand Down Expand Up @@ -196,7 +220,7 @@ async fn standalone_image_generation_failure_emits_terminal_item() -> Result<()>
.build()
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
start_image_generation_turn(&mut mcp).await?;
start_image_generation_turn(&mut mcp, ThreadStartParams::default()).await?;

let completed = timeout(
DEFAULT_READ_TIMEOUT,
Expand Down Expand Up @@ -325,7 +349,7 @@ async fn standalone_image_generation_is_exposed_in_code_mode_only() -> Result<()
.build()
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
start_image_generation_turn(&mut mcp).await?;
start_image_generation_turn(&mut mcp, ThreadStartParams::default()).await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
Expand Down Expand Up @@ -391,7 +415,7 @@ generatedImage(result);
.build()
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
start_image_generation_turn(&mut mcp).await?;
start_image_generation_turn(&mut mcp, ThreadStartParams::default()).await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
Expand Down Expand Up @@ -420,9 +444,13 @@ generatedImage(result);
Ok(())
}

async fn start_image_generation_turn(mcp: &mut TestAppServer) -> Result<()> {
async fn start_image_generation_turn(
mcp: &mut TestAppServer,
thread_start_params: ThreadStartParams,
) -> Result<()> {
start_turn(
mcp,
thread_start_params,
vec![V2UserInput::Text {
text: "Generate an image".to_string(),
text_elements: Vec::new(),
Expand Down Expand Up @@ -474,7 +502,15 @@ async fn run_image_edit_test(
.build()
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
start_turn(&mut mcp, input).await?;
start_turn(
&mut mcp,
ThreadStartParams {
service_name: Some("chatgpt_cca".to_string()),
..Default::default()
},
input,
)
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
wait_for_image_generation_completed(&mut mcp),
Expand All @@ -491,16 +527,29 @@ async fn run_image_edit_test(
.received_requests()
.await
.context("failed to fetch received requests")?;
Ok(requests
let image_request = requests
.iter()
.find(|request| request.url.path() == "/api/codex/images/edits")
.context("image edit request should be sent")?
.body_json::<serde_json::Value>()?)
.context("image edit request should be sent")?;
assert_eq!(
image_request
.headers
.get("originator")
.context("standalone image edit should include the thread originator")?
.to_str()
.context("standalone image edit originator should be valid ASCII")?,
"chatgpt_cca"
);
Ok(image_request.body_json::<serde_json::Value>()?)
}

async fn start_turn(mcp: &mut TestAppServer, input: Vec<V2UserInput>) -> Result<()> {
async fn start_turn(
mcp: &mut TestAppServer,
thread_start_params: ThreadStartParams,
input: Vec<V2UserInput>,
) -> Result<()> {
let thread_req = mcp
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
.send_thread_start_request_with_auto_env(thread_start_params)
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
Expand Down
14 changes: 13 additions & 1 deletion codex-rs/app-server/tests/suite/v2/web_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

let thread_req = mcp
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
.send_thread_start_request_with_auto_env(ThreadStartParams {
service_name: Some("chatgpt_cca".to_string()),
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
Expand Down Expand Up @@ -159,6 +162,15 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
);

let search_request = search_request(&server).await?;
assert_eq!(
search_request
.headers
.get("originator")
.context("standalone search should include the thread originator")?
.to_str()
.context("standalone search originator should be valid ASCII")?,
"chatgpt_cca"
);
let search_body = search_request
.body_json::<Value>()
.context("search request body should be JSON")?;
Expand Down
17 changes: 1 addition & 16 deletions codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::RefreshTokenError;
use codex_login::UnauthorizedRecovery;
use codex_login::default_client::add_originator_header;
use codex_login::default_client::build_default_reqwest_client_for_route;
use codex_otel::SessionTelemetry;
use codex_otel::current_span_w3c_trace_context;
Expand Down Expand Up @@ -1884,22 +1885,6 @@ fn build_responses_headers(
headers
}

pub(crate) fn add_originator_header(headers: &mut ApiHeaderMap, originator: &str) {
let default_originator = codex_login::default_client::originator();
if originator == default_originator.value.as_str() {
return;
}

match HeaderValue::from_str(originator) {
Ok(header_value) => {
headers.insert("originator", header_value);
}
Err(err) => {
warn!("ignoring invalid thread originator header value: {err}");
}
}
}

fn add_responses_lite_header(headers: &mut ApiHeaderMap, use_responses_lite: bool) {
if use_responses_lite {
headers.insert(
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/realtime_conversation.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::client::ModelClient;
use crate::client::add_originator_header;
use crate::realtime_context::build_realtime_startup_context;
use crate::realtime_context::truncate_realtime_text_to_token_budget;
use crate::realtime_prompt::prepare_realtime_backend_prompt;
Expand All @@ -25,6 +24,7 @@ use codex_api::map_api_error;
use codex_config::config_toml::RealtimeWsMode;
use codex_config::config_toml::RealtimeWsVersion;
use codex_login::CodexAuth;
use codex_login::default_client::add_originator_header;
use codex_login::default_client::default_headers;
use codex_login::read_openai_api_key_from_env;
use codex_model_provider_info::ModelProviderInfo;
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/src/session/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,9 @@ impl Session {
roots
}
};
thread_extension_init.insert(codex_extension_api::ThreadOriginator(
session_configuration.originator.clone(),
));
let mcp_thread_init = thread_extension_init.clone();
let thread_extension_data = codex_extension_api::ExtensionData::new_with_init(
thread_id.to_string(),
Expand Down
8 changes: 7 additions & 1 deletion codex-rs/core/src/thread_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,13 @@ pub struct StartThreadOptions {

fn originator_from_service_name(service_name: Option<&str>) -> Option<String> {
let service_name = service_name?.trim();
for originator in ["codex_work_desktop", "codex_work_web", "codex_work_mobile"] {
for originator in [
"codex_work_desktop",
"codex_work_web",
"codex_work_mobile",
"codex_work_cca",
"chatgpt_cca",
] {
if service_name.eq_ignore_ascii_case(originator) {
return Some(originator.to_string());
}
Expand Down
18 changes: 18 additions & 0 deletions codex-rs/core/src/thread_manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ fn effective_originator_prefers_thread_scoped_sources_before_env_originator() {
Some("inherited_originator"),
"codex_work_mobile",
),
(
Some("codex_work_cca"),
Some("persisted_originator"),
Some("inherited_originator"),
"codex_work_cca",
),
(
Some("chatgpt_cca"),
Some("persisted_originator"),
Some("inherited_originator"),
"chatgpt_cca",
),
(
Some("chatgpt_cca_extra"),
Some("persisted_originator"),
Some("inherited_originator"),
"persisted_originator",
),
(
None,
Some("persisted_originator"),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/ext/extension-api/src/contributors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub use prompt::PromptSlot;
pub use skill_invocation::SkillInvocationInput;
pub use skill_invocation::SkillInvocationKind;
pub use thread_lifecycle::ThreadIdleInput;
pub use thread_lifecycle::ThreadOriginator;
pub use thread_lifecycle::ThreadResumeInput;
pub use thread_lifecycle::ThreadStartInput;
pub use thread_lifecycle::ThreadStopInput;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ use crate::ExtensionData;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TurnEnvironmentSelection;

/// Trusted, host-resolved billing attribution for a thread.
///
/// Extensions may forward this value to first-party APIs. It is seeded by Core
/// after resolving persisted and host-provided originator state, rather than
/// from model- or tool-controlled input.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ThreadOriginator(pub String);

/// Input supplied when the host starts a runtime for a thread.
pub struct ThreadStartInput<'a, C> {
/// Host configuration visible at thread start.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/ext/extension-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub use contributors::SkillInvocationInput;
pub use contributors::SkillInvocationKind;
pub use contributors::ThreadIdleInput;
pub use contributors::ThreadLifecycleContributor;
pub use contributors::ThreadOriginator;
pub use contributors::ThreadResumeInput;
pub use contributors::ThreadStartInput;
pub use contributors::ThreadStopInput;
Expand Down
21 changes: 17 additions & 4 deletions codex-rs/ext/image-generation/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ use codex_api::ImageGenerationRequest;
use codex_api::ImageResponse;
use codex_api::ImagesClient;
use codex_api::ReqwestTransport;
use codex_login::default_client::add_originator_header;
use codex_login::default_client::build_reqwest_client;
use codex_model_provider::SharedModelProvider;
use http::HeaderMap;

#[derive(Clone)]
pub(crate) struct CodexImagesBackend {
provider: SharedModelProvider,
originator: Option<String>,
}

impl CodexImagesBackend {
/// Creates a backend that sends image requests through the active model provider.
pub(crate) fn new(provider: SharedModelProvider) -> Self {
Self { provider }
pub(crate) fn new(provider: SharedModelProvider, originator: Option<String>) -> Self {
Self {
provider,
originator,
}
}

/// Resolves the provider and auth required for the current image API request.
Expand Down Expand Up @@ -44,7 +49,7 @@ impl CodexImagesBackend {
) -> Result<ImageResponse, String> {
self.client()
.await?
.generate(&request, HeaderMap::new())
.generate(&request, image_request_headers(self.originator.as_deref()))
.await
.map_err(|err| err.to_string())
}
Expand All @@ -53,8 +58,16 @@ impl CodexImagesBackend {
pub(crate) async fn edit(&self, request: ImageEditRequest) -> Result<ImageResponse, String> {
self.client()
.await?
.edit(&request, HeaderMap::new())
.edit(&request, image_request_headers(self.originator.as_deref()))
.await
.map_err(|err| err.to_string())
}
}

fn image_request_headers(originator: Option<&str>) -> HeaderMap {
let mut headers = HeaderMap::new();
if let Some(originator) = originator {
add_originator_header(&mut headers, originator);
}
headers
}
11 changes: 7 additions & 4 deletions codex-rs/ext/image-generation/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionFuture;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::ThreadLifecycleContributor;
use codex_extension_api::ThreadOriginator;
use codex_extension_api::ThreadStartInput;
use codex_extension_api::ToolCall;
use codex_extension_api::ToolContributor;
Expand Down Expand Up @@ -94,10 +95,12 @@ impl ToolContributor for ImageGenerationExtension {
}

vec![Arc::new(ImageGenerationTool::new(
CodexImagesBackend::new(create_model_provider(
config.provider.clone(),
Some(self.auth_manager.clone()),
)),
CodexImagesBackend::new(
create_model_provider(config.provider.clone(), Some(self.auth_manager.clone())),
thread_store
.get::<ThreadOriginator>()
.map(|originator| originator.0.clone()),
),
config.save_root.clone(),
thread_store.level_id().to_string(),
))]
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/ext/web-search/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionFuture;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::ThreadLifecycleContributor;
use codex_extension_api::ThreadOriginator;
use codex_extension_api::ThreadStartInput;
use codex_extension_api::ToolContributor;
use codex_login::AuthManager;
Expand Down Expand Up @@ -134,6 +135,9 @@ impl ToolContributor for WebSearchExtension {
Some(self.auth_manager.clone()),
),
settings: config.settings.clone(),
originator: thread_store
.get::<ThreadOriginator>()
.map(|originator| originator.0.clone()),
})]
}
}
Expand Down
Loading
Loading