Allow callers to set the thread source for sampling (#56364)

## What changed

Add optional `threadSource` to `thread/sample` requests and apply it to the ephemeral fork. Keep `thread_sample` as the default when omitted.

## Testing

Extend sampling tests to assert that turn metadata includes the supplied thread source and retains the default when no source is provided.

GitOrigin-RevId: 8ea941c932267e3bcb9e6b086764ac2795d6edef
This commit is contained in:
Keyan Zhang
2026-10-23 06:07:10 +00:00
committed by copyberry
parent 30587d7885
commit a96fa4f220
4 changed files with 13 additions and 1 deletions
@@ -1,5 +1,6 @@
//! One model response on an ephemeral fork that inherits the source's configuration and context.
use super::ThreadSource;
use crate::JsonSchema;
use crate::RequestId;
use crate::TS;
@@ -21,6 +22,8 @@ pub struct ThreadSampleParams {
#[ts(optional = nullable)]
pub effort: Option<ReasoningEffort>,
#[ts(optional = nullable)]
pub thread_source: Option<ThreadSource>,
#[ts(optional = nullable)]
pub turn_trigger: Option<String>,
/// Optional metadata to enrich Codex's ResponsesAPI turn metadata.
#[ts(optional = nullable)]
+6 -1
View File
@@ -213,7 +213,12 @@ impl SampleTask {
model.supports_reasoning_effort_updates && options.config.model_provider.is_openai()
});
options.session_source = Some(SessionSource::Custom("thread_sample".to_string()));
options.thread_source = Some(ThreadSource::Feature("thread_sample".to_string()));
options.thread_source = Some(
params
.thread_source
.map(Into::into)
.unwrap_or_else(|| ThreadSource::Feature("thread_sample".to_string())),
);
options.parent_trace = trace.clone();
if cancellation.is_cancelled() {
return Err(internal_error("sample canceled"));
@@ -16,6 +16,7 @@ use codex_app_server_protocol::ThreadLoadedListResponse;
use codex_app_server_protocol::ThreadSampleCancelResponse;
use codex_app_server_protocol::ThreadSampleParams;
use codex_app_server_protocol::ThreadSampleResponse;
use codex_app_server_protocol::ThreadSource;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnInterruptParams;
@@ -304,6 +305,7 @@ async fn sample_preserves_context_and_keeps_source_unchanged(
expected_turn_id: completed.turn.id.clone(),
prompt: "Continue identically".to_string(),
effort: Some(ReasoningEffort::Medium),
thread_source: Some(ThreadSource::Feature("composer_predictions".to_string())),
turn_trigger: Some("composer_predictions".to_string()),
responsesapi_client_metadata: Some(HashMap::from([
(
@@ -390,6 +392,7 @@ async fn sample_preserves_context_and_keeps_source_unchanged(
.header("x-codex-turn-metadata")
.expect("turn metadata"),
)?;
assert_eq!(metadata["thread_source"], "composer_predictions");
assert_eq!(metadata["turn_trigger"], "composer_predictions");
assert_eq!(metadata["generation_trigger"], "composer_prediction");
let body_metadata: Value = serde_json::from_str(
@@ -513,6 +516,7 @@ async fn sample_preserves_effort_baseline(
.header("x-codex-turn-metadata")
.expect("turn metadata"),
)?;
assert_eq!(metadata["thread_source"], "thread_sample");
assert!(metadata.get("turn_trigger").is_none());
Ok(())
}