mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Support Daybreak selection in codex exec (#49856)
## What changed - Add the `daybreak` configuration key, defaulting to `false`, with per-invocation overrides such as `-c daybreak=true`. - Resolve access programs from the model catalog, preferring `daybreak_blue` over `daybreak_red` when enabled and selecting advertised `standard` access when disabled for signed-in ChatGPT accounts. Require the OpenAI provider, a signed-in ChatGPT account, and a compatible model to enable Daybreak. - Save the choice for new persistent threads and inherit it on resume and fork, including ephemeral forks. Explicit CLI overrides apply only to the current invocation. ## Testing Add protocol tests for advertised-program selection and exec integration tests for account and provider restrictions, saved choices, ephemeral forks, and invocation-only overrides. GitOrigin-RevId: ac955f08b5a891e720cfc5f67fabb7b0f6d32bb8
This commit is contained in:
@@ -166,6 +166,8 @@ pub struct FeatureToggleToml {
|
||||
pub struct ConfigToml {
|
||||
/// Optional override of model selection.
|
||||
pub model: Option<String>,
|
||||
/// Default Daybreak preference for new threads and non-interactive turns.
|
||||
pub daybreak: Option<bool>,
|
||||
/// Review model override used by the `/review` feature.
|
||||
pub review_model: Option<String>,
|
||||
|
||||
|
||||
@@ -6742,6 +6742,10 @@
|
||||
"computer_use": {
|
||||
"$ref": "#/definitions/ComputerUseConfigToml"
|
||||
},
|
||||
"daybreak": {
|
||||
"description": "Default Daybreak preference for new threads and non-interactive turns.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"default_permissions": {
|
||||
"description": "Default permissions profile to apply. Names starting with `:` refer to built-in profiles; other names are resolved from the `[permissions]` table.",
|
||||
"type": "string"
|
||||
|
||||
@@ -623,6 +623,9 @@ pub struct Config {
|
||||
/// Optional override of model selection.
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Default Daybreak preference for new threads and non-interactive turns.
|
||||
pub daybreak_enabled: bool,
|
||||
|
||||
/// Effective service tier request id preference for new turns.
|
||||
/// `default` means the user explicitly selected standard routing.
|
||||
pub service_tier: Option<String>,
|
||||
@@ -4282,6 +4285,7 @@ impl Config {
|
||||
let config = Self {
|
||||
prefer_mxc,
|
||||
model,
|
||||
daybreak_enabled: cfg.daybreak.unwrap_or(false),
|
||||
service_tier,
|
||||
review_model,
|
||||
model_context_window: cfg.model_context_window,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//! Resolve the cyber treatment for an exec turn from the connected server's catalog.
|
||||
|
||||
use crate::RequestIdSequencer;
|
||||
use codex_app_server_client::InProcessAppServerClient;
|
||||
use codex_app_server_protocol::Account;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::CyberAccessProgram;
|
||||
use codex_app_server_protocol::GetAccountParams;
|
||||
use codex_app_server_protocol::GetAccountResponse;
|
||||
use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
use codex_protocol::openai_models::ModelAccessPrograms;
|
||||
|
||||
pub(crate) async fn program_for_turn(
|
||||
client: &InProcessAppServerClient,
|
||||
request_ids: &mut RequestIdSequencer,
|
||||
model: &str,
|
||||
provider: &str,
|
||||
enabled: bool,
|
||||
) -> anyhow::Result<Option<CyberAccessProgram>> {
|
||||
if provider != "openai" {
|
||||
anyhow::ensure!(!enabled, "Daybreak requires the OpenAI model provider");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if enabled {
|
||||
anyhow::ensure!(
|
||||
has_chatgpt_account(client, request_ids).await?,
|
||||
"Daybreak requires a signed-in ChatGPT account"
|
||||
);
|
||||
}
|
||||
|
||||
let mut cursor = None;
|
||||
loop {
|
||||
let result = client
|
||||
.request_typed::<ModelListResponse>(ClientRequest::ModelList {
|
||||
request_id: request_ids.next(),
|
||||
params: ModelListParams {
|
||||
cursor,
|
||||
limit: None,
|
||||
include_hidden: Some(true),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
let Ok(result) = result else {
|
||||
anyhow::ensure!(!enabled, "Daybreak availability could not be determined");
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(selected) = result.data.into_iter().find(|entry| entry.model == model) {
|
||||
let programs = selected
|
||||
.available_access_programs
|
||||
.map(ModelAccessPrograms::from);
|
||||
if enabled {
|
||||
return programs
|
||||
.and_then(|programs| programs.daybreak())
|
||||
.map(|program| Some(program.into()))
|
||||
.ok_or_else(|| anyhow::anyhow!("Daybreak is unavailable for model {model}; turn it off or choose a compatible model"));
|
||||
}
|
||||
let Some(standard) = programs.and_then(|programs| programs.standard()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
return Ok(has_chatgpt_account(client, request_ids)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
.then_some(standard.into()));
|
||||
}
|
||||
match result.next_cursor {
|
||||
Some(next) => cursor = Some(next),
|
||||
None => {
|
||||
anyhow::ensure!(
|
||||
!enabled,
|
||||
"Daybreak availability could not be determined for model {model}"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn has_chatgpt_account(
|
||||
client: &InProcessAppServerClient,
|
||||
request_ids: &mut RequestIdSequencer,
|
||||
) -> anyhow::Result<bool> {
|
||||
let account = client
|
||||
.request_typed::<GetAccountResponse>(ClientRequest::GetAccount {
|
||||
request_id: request_ids.next(),
|
||||
params: GetAccountParams {
|
||||
refresh_token: false,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
anyhow::anyhow!("Daybreak account availability could not be determined: {error}")
|
||||
})?;
|
||||
Ok(matches!(account.account, Some(Account::Chatgpt { .. })))
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#![deny(clippy::print_stdout)]
|
||||
|
||||
mod cli;
|
||||
mod daybreak;
|
||||
mod event_processor;
|
||||
mod event_processor_with_human_output;
|
||||
pub(crate) mod event_processor_with_jsonl_output;
|
||||
@@ -218,6 +219,7 @@ struct ExecRunArgs {
|
||||
command: Option<ExecCommand>,
|
||||
config: Config,
|
||||
resume_approvals_reviewer_override: Option<codex_app_server_protocol::ApprovalsReviewer>,
|
||||
daybreak_override: Option<bool>,
|
||||
dangerously_bypass_approvals_and_sandbox: bool,
|
||||
exec_span: tracing::Span,
|
||||
images: Vec<PathBuf>,
|
||||
@@ -615,6 +617,10 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
)
|
||||
.await?;
|
||||
embedded_network_policy.activate(&mut config);
|
||||
let daybreak_override = cli_kv_overrides
|
||||
.iter()
|
||||
.any(|(key, _)| key == "daybreak")
|
||||
.then_some(config.daybreak_enabled);
|
||||
let resume_approvals_reviewer_override = cli_kv_overrides
|
||||
.iter()
|
||||
.any(|(key, _)| key == "approvals_reviewer")
|
||||
@@ -737,6 +743,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
command,
|
||||
config,
|
||||
resume_approvals_reviewer_override,
|
||||
daybreak_override,
|
||||
dangerously_bypass_approvals_and_sandbox,
|
||||
exec_span: exec_span.clone(),
|
||||
images,
|
||||
@@ -837,6 +844,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
command,
|
||||
config,
|
||||
resume_approvals_reviewer_override,
|
||||
daybreak_override,
|
||||
dangerously_bypass_approvals_and_sandbox,
|
||||
exec_span,
|
||||
images,
|
||||
@@ -852,6 +860,16 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
thread_source,
|
||||
} = args;
|
||||
|
||||
if config.daybreak_enabled && !matches!(&command, Some(ExecCommand::Review(_))) {
|
||||
anyhow::ensure!(
|
||||
!oss || matches!(
|
||||
&command,
|
||||
Some(ExecCommand::Resume(_) | ExecCommand::Fork(_))
|
||||
),
|
||||
"Daybreak requires the OpenAI model provider"
|
||||
);
|
||||
}
|
||||
|
||||
let mut event_processor: Box<dyn EventProcessor> = match json_mode {
|
||||
true => Box::new(EventProcessorWithJsonOutput::new(last_message_file.clone())),
|
||||
_ => Box::new(EventProcessorWithHumanOutput::create_with_ansi(
|
||||
@@ -989,6 +1007,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
anyhow::anyhow!("failed to initialize in-process app-server client: {err}")
|
||||
})?;
|
||||
|
||||
let mut daybreak_enabled = config.daybreak_enabled;
|
||||
// Resolve resume and fork through existing app-server thread lifecycle APIs.
|
||||
let (primary_thread_id, fallback_session_configured) = if let Some(ExecCommand::Resume(args)) =
|
||||
command.as_ref()
|
||||
@@ -1013,6 +1032,9 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
let session_configured =
|
||||
session_configured_from_thread_resume_response(&response, &config)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
daybreak_enabled = daybreak_override
|
||||
.or(response.thread.daybreak_enabled)
|
||||
.unwrap_or(false);
|
||||
(session_configured.thread_id, session_configured)
|
||||
} else {
|
||||
let response = start_thread(&client, &mut request_ids, &config, &thread_source)
|
||||
@@ -1035,6 +1057,24 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
resolve_resume_thread_id(&client, &config, state_db.as_ref(), &source_args)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Session not found: {}", args.session_id))?;
|
||||
let source_daybreak = if config.ephemeral && daybreak_override.is_none() {
|
||||
let response: ThreadReadResponse = send_request_with_response(
|
||||
&client,
|
||||
ClientRequest::ThreadRead {
|
||||
request_id: request_ids.next(),
|
||||
params: ThreadReadParams {
|
||||
thread_id: source_thread_id.clone(),
|
||||
include_turns: false,
|
||||
},
|
||||
},
|
||||
"thread/read",
|
||||
)
|
||||
.await
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
response.thread.daybreak_enabled
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let permissions = permissions_selection_from_config(&config);
|
||||
let sandbox = permissions.is_none().then(|| {
|
||||
sandbox_mode_from_permission_profile(
|
||||
@@ -1087,6 +1127,10 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
response.reasoning_effort,
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
daybreak_enabled = daybreak_override
|
||||
.or(response.thread.daybreak_enabled)
|
||||
.or(source_daybreak)
|
||||
.unwrap_or(false);
|
||||
(session_configured.thread_id, session_configured)
|
||||
} else {
|
||||
let response = start_thread(&client, &mut request_ids, &config, &thread_source)
|
||||
@@ -1147,6 +1191,14 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
items,
|
||||
output_schema,
|
||||
} => {
|
||||
let cyber_access_program = daybreak::program_for_turn(
|
||||
&client,
|
||||
&mut request_ids,
|
||||
&session_configured.model,
|
||||
&session_configured.model_provider_id,
|
||||
daybreak_enabled,
|
||||
)
|
||||
.await?;
|
||||
let response: TurnStartResponse = send_request_with_response(
|
||||
&client,
|
||||
ClientRequest::TurnStart {
|
||||
@@ -1176,7 +1228,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
output_schema,
|
||||
collaboration_mode: None,
|
||||
multi_agent_mode: None,
|
||||
cyber_access_program: None,
|
||||
cyber_access_program,
|
||||
},
|
||||
},
|
||||
"turn/start",
|
||||
@@ -1377,6 +1429,7 @@ fn thread_start_params_from_config(
|
||||
permissions,
|
||||
config: thread_config_overrides_from_config(config),
|
||||
ephemeral: Some(config.ephemeral),
|
||||
daybreak_enabled: (config.daybreak_enabled && !config.ephemeral).then_some(true),
|
||||
history_mode: (!config.ephemeral).then_some(ThreadHistoryMode::Paginated),
|
||||
thread_source: Some(thread_source.clone()),
|
||||
..ThreadStartParams::default()
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
use anyhow::Context;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::test_codex_exec::TestCodexExecBuilder;
|
||||
use core_test_support::test_codex_exec::test_codex_exec;
|
||||
use predicates::prelude::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
fn started_thread(output: std::process::Output) -> anyhow::Result<String> {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
String::from_utf8(output.stdout)?
|
||||
.lines()
|
||||
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
|
||||
.find(|event| event["type"] == "thread.started")
|
||||
.and_then(|event| event["thread_id"].as_str().map(str::to_owned))
|
||||
.context("exec should report its thread ID")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_rejects_daybreak_for_api_keys_and_allows_an_override() -> anyhow::Result<()> {
|
||||
let (test, server) = configured_daybreak_exec().await?;
|
||||
std::fs::remove_file(test.home_path().join("auth.json"))?;
|
||||
let response = responses::mount_sse_once(&server, responses::sse_completed("response")).await;
|
||||
|
||||
test.cmd_with_server(&server)
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.arg("--skip-git-repo-check")
|
||||
.arg("hello")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"Daybreak requires a signed-in ChatGPT account",
|
||||
));
|
||||
assert!(response.requests().is_empty());
|
||||
|
||||
test.cmd_with_server(&server)
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.arg("--skip-git-repo-check")
|
||||
.arg("-c")
|
||||
.arg("daybreak=false")
|
||||
.arg("hello")
|
||||
.assert()
|
||||
.success();
|
||||
assert_eq!(
|
||||
response.single_request().body_json().get("access_programs"),
|
||||
None
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_rejects_daybreak_for_non_openai_provider() -> anyhow::Result<()> {
|
||||
let test = test_codex_exec();
|
||||
let server = responses::start_mock_server().await;
|
||||
std::fs::write(test.home_path().join("config.toml"), "daybreak = true\n")?;
|
||||
let response = responses::mount_sse_once(&server, responses::sse_completed("response")).await;
|
||||
|
||||
test.cmd_with_server(&server)
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.args([
|
||||
"--oss",
|
||||
"--local-provider",
|
||||
"ollama",
|
||||
"--skip-git-repo-check",
|
||||
"hello",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"Daybreak requires the OpenAI model provider",
|
||||
));
|
||||
assert!(response.requests().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn configured_daybreak_exec() -> anyhow::Result<(TestCodexExecBuilder, wiremock::MockServer)>
|
||||
{
|
||||
let test = test_codex_exec();
|
||||
let server = responses::start_mock_server().await;
|
||||
let token = concat!(
|
||||
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.",
|
||||
"eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9wbGFuX3R5cGUiOiJlbnRlcnByaXNlIiwiY2hhdGdwdF9hY2NvdW50X2lkIjoid29ya3NwYWNlIn19.",
|
||||
"c2lnbmF0dXJl",
|
||||
);
|
||||
std::fs::write(
|
||||
test.home_path().join("auth.json"),
|
||||
serde_json::to_vec(&json!({
|
||||
"auth_mode": "chatgpt",
|
||||
"tokens": {"id_token": token, "access_token": "test-token",
|
||||
"refresh_token": "refresh-token", "account_id": "workspace"},
|
||||
"last_refresh": "2099-01-01T00:00:00Z"
|
||||
}))?,
|
||||
)?;
|
||||
let catalog = test.home_path().join("catalog.json");
|
||||
std::fs::write(
|
||||
&catalog,
|
||||
serde_json::to_vec(&json!({"models": [{
|
||||
"slug": "gpt-test", "display_name": "Test", "base_instructions": "Test instructions", "supported_reasoning_levels": [],
|
||||
"shell_type": "disabled", "visibility": "list", "supported_in_api": true,
|
||||
"priority": 0, "support_verbosity": false,
|
||||
"truncation_policy": {"mode": "tokens", "limit": 10000},
|
||||
"experimental_supported_tools": [],
|
||||
"available_access_programs": {"cyber": ["standard", "daybreak_red", "daybreak_blue"]}
|
||||
}]}))?,
|
||||
)?;
|
||||
std::fs::write(
|
||||
test.home_path().join("config.toml"),
|
||||
format!(
|
||||
"cli_auth_credentials_store = 'file'\nchatgpt_base_url = '{}/backend-api'\nmodel_catalog_json = '{}'\nmodel = 'gpt-test'\ndaybreak = true\n",
|
||||
server.uri(),
|
||||
catalog.display()
|
||||
),
|
||||
)?;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/backend-api/wham/config/bundle"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(json!({
|
||||
"config_toml": {"enterprise_managed": []}
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/backend-api/wham/accounts/check"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(json!({
|
||||
"accounts": [{"id": "workspace", "workspace_backend_origin": "https://chatgpt.com",
|
||||
"account_routing_override": "NO_CONSTRAINT"}]
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Ok((test, server))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_resumes_and_forks_the_saved_daybreak_choice() -> anyhow::Result<()> {
|
||||
let (test, server) = configured_daybreak_exec().await?;
|
||||
let response = responses::mount_sse_once(&server, responses::sse_completed("started")).await;
|
||||
let output = test
|
||||
.cmd_with_server(&server)
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.env_remove("CODEX_API_KEY")
|
||||
.env_remove("OPENAI_API_KEY")
|
||||
.args(["--skip-git-repo-check", "--json", "hello"])
|
||||
.output()?;
|
||||
let thread_id = started_thread(output)?;
|
||||
assert_eq!(
|
||||
response.single_request().body_json()["access_programs"],
|
||||
json!({"cyber": "daybreak_blue"})
|
||||
);
|
||||
|
||||
// The saved choice wins over a changed default; a CLI override affects only its invocation.
|
||||
std::fs::write(
|
||||
test.home_path().join("config.toml"),
|
||||
format!(
|
||||
"cli_auth_credentials_store = 'file'\nchatgpt_base_url = '{}/backend-api'\nmodel_catalog_json = '{}'\nmodel = 'gpt-test'\n",
|
||||
server.uri(),
|
||||
test.home_path().join("catalog.json").display()
|
||||
),
|
||||
)?;
|
||||
for (args, expected) in [
|
||||
(
|
||||
vec![
|
||||
"-c",
|
||||
"daybreak=false",
|
||||
"resume",
|
||||
thread_id.as_str(),
|
||||
"continue",
|
||||
],
|
||||
"standard",
|
||||
),
|
||||
(
|
||||
vec!["resume", thread_id.as_str(), "continue"],
|
||||
"daybreak_blue",
|
||||
),
|
||||
(
|
||||
vec!["fork", thread_id.as_str(), "continue"],
|
||||
"daybreak_blue",
|
||||
),
|
||||
(
|
||||
vec!["--ephemeral", "fork", thread_id.as_str(), "continue"],
|
||||
"daybreak_blue",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--ephemeral",
|
||||
"-c",
|
||||
"daybreak=false",
|
||||
"fork",
|
||||
thread_id.as_str(),
|
||||
"continue",
|
||||
],
|
||||
"standard",
|
||||
),
|
||||
] {
|
||||
let response =
|
||||
responses::mount_sse_once(&server, responses::sse_completed("continued")).await;
|
||||
test.cmd_with_server(&server)
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.env_remove("CODEX_API_KEY")
|
||||
.env_remove("OPENAI_API_KEY")
|
||||
.arg("--skip-git-repo-check")
|
||||
.args(args)
|
||||
.assert()
|
||||
.success();
|
||||
assert_eq!(
|
||||
response.single_request().body_json()["access_programs"],
|
||||
json!({"cyber": expected})
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_resume_override_does_not_change_a_standard_thread() -> anyhow::Result<()> {
|
||||
let (test, server) = configured_daybreak_exec().await?;
|
||||
let response =
|
||||
responses::mount_sse_once(&server, responses::sse_completed("started-standard")).await;
|
||||
let output = test
|
||||
.cmd_with_server(&server)
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.env_remove("CODEX_API_KEY")
|
||||
.env_remove("OPENAI_API_KEY")
|
||||
.args([
|
||||
"--skip-git-repo-check",
|
||||
"--json",
|
||||
"-c",
|
||||
"daybreak=false",
|
||||
"hello",
|
||||
])
|
||||
.output()?;
|
||||
let thread_id = started_thread(output)?;
|
||||
assert_eq!(
|
||||
response.single_request().body_json()["access_programs"],
|
||||
json!({"cyber": "standard"})
|
||||
);
|
||||
for (args, expected) in [
|
||||
(
|
||||
vec![
|
||||
"-c",
|
||||
"daybreak=true",
|
||||
"resume",
|
||||
thread_id.as_str(),
|
||||
"continue",
|
||||
],
|
||||
"daybreak_blue",
|
||||
),
|
||||
(vec!["resume", thread_id.as_str(), "continue"], "standard"),
|
||||
] {
|
||||
let response =
|
||||
responses::mount_sse_once(&server, responses::sse_completed("continued-standard"))
|
||||
.await;
|
||||
test.cmd_with_server(&server)
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.env_remove("CODEX_API_KEY")
|
||||
.env_remove("OPENAI_API_KEY")
|
||||
.arg("--skip-git-repo-check")
|
||||
.args(args)
|
||||
.assert()
|
||||
.success();
|
||||
assert_eq!(
|
||||
response.single_request().body_json()["access_programs"],
|
||||
json!({"cyber": expected})
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,6 +6,7 @@ mod approval_policy;
|
||||
mod auth_env;
|
||||
#[path = "completion_backfill_tests.rs"]
|
||||
mod completion_backfill;
|
||||
mod daybreak;
|
||||
mod ephemeral;
|
||||
mod hooks;
|
||||
mod mcp_required_exit;
|
||||
|
||||
@@ -17,6 +17,25 @@ pub struct ModelAccessPrograms {
|
||||
pub cyber: Vec<CyberAccessProgram>,
|
||||
}
|
||||
|
||||
impl ModelAccessPrograms {
|
||||
/// Select the preferred Daybreak treatment advertised for this model.
|
||||
pub fn daybreak(&self) -> Option<CyberAccessProgram> {
|
||||
[
|
||||
CyberAccessProgram::DaybreakBlue,
|
||||
CyberAccessProgram::DaybreakRed,
|
||||
]
|
||||
.into_iter()
|
||||
.find(|program| self.cyber.contains(program))
|
||||
}
|
||||
|
||||
/// Select the standard treatment when it is advertised.
|
||||
pub fn standard(&self) -> Option<CyberAccessProgram> {
|
||||
self.cyber
|
||||
.contains(&CyberAccessProgram::Standard)
|
||||
.then_some(CyberAccessProgram::Standard)
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_known_cyber_access_programs<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<CyberAccessProgram>, D::Error>
|
||||
@@ -29,3 +48,7 @@ where
|
||||
.filter_map(|program| serde_json::from_value(serde_json::Value::String(program)).ok())
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "access_programs_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use super::ModelAccessPrograms;
|
||||
use crate::turn_input::CyberAccessProgram;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn selects_only_advertised_programs_with_blue_preferred() {
|
||||
use CyberAccessProgram::*;
|
||||
for (cyber, expected_daybreak, expected_standard) in [
|
||||
(
|
||||
vec![Standard, DaybreakRed, DaybreakBlue],
|
||||
Some(DaybreakBlue),
|
||||
Some(Standard),
|
||||
),
|
||||
(vec![DaybreakRed], Some(DaybreakRed), None),
|
||||
(vec![Standard], None, Some(Standard)),
|
||||
(vec![], None, None),
|
||||
] {
|
||||
let programs = ModelAccessPrograms { cyber };
|
||||
assert_eq!(
|
||||
(programs.daybreak(), programs.standard()),
|
||||
(expected_daybreak, expected_standard)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -242,6 +242,7 @@ async fn new_config(
|
||||
startup_warnings: Vec::new(),
|
||||
bypass_hook_trust: false,
|
||||
model,
|
||||
daybreak_enabled: false,
|
||||
service_tier: None,
|
||||
review_model: None,
|
||||
model_context_window: None,
|
||||
|
||||
Reference in New Issue
Block a user