mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Add an option to suppress incremental model change announcements (#58405)
## What changed Add the default-off `suppress_model_change_announcements` feature flag. When enabled, incremental context updates omit the model ID announcement after a model switch while still delivering changed base instructions and tool definitions. Advance the model snapshot even when a switch emits no context update, so subsequent turns recognize the current model. ## Testing Add coverage for omitted and explicit flag values, snapshot advancement without repeated updates, and request history across model switches, repeated turns, and compaction with announcements suppressed. GitOrigin-RevId: 89c70f1e9d7981fd945fb8e41b1f98492717a07e
This commit is contained in:
@@ -1368,6 +1368,9 @@
|
||||
"subagent_thread_environments": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"suppress_model_change_announcements": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"system_proxy_fallback": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -8069,6 +8072,9 @@
|
||||
"subagent_thread_environments": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"suppress_model_change_announcements": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"system_proxy_fallback": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(crate) struct ModelInstructionsState {
|
||||
/// Ordinary-window fallback when the saved model snapshot is unavailable.
|
||||
previous_model: Option<String>,
|
||||
instructions: String,
|
||||
suppress_model_change_announcements: bool,
|
||||
}
|
||||
|
||||
impl ModelInstructionsState {
|
||||
@@ -40,10 +41,15 @@ impl ModelInstructionsState {
|
||||
snapshot: ModelInstructionsSnapshot::ModelOnly(model.to_string()),
|
||||
previous_model: previous_model.map(str::to_string),
|
||||
instructions,
|
||||
suppress_model_change_announcements: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn for_incremental_tools(model: &str, instructions: String) -> Self {
|
||||
pub(crate) fn for_incremental_tools(
|
||||
model: &str,
|
||||
instructions: String,
|
||||
suppress_model_change_announcements: bool,
|
||||
) -> Self {
|
||||
let instructions = BaseInstructionsFragment(instructions);
|
||||
let instructions_hash = WorldStateHash::from_fragment(&instructions);
|
||||
Self {
|
||||
@@ -53,6 +59,7 @@ impl ModelInstructionsState {
|
||||
},
|
||||
previous_model: None,
|
||||
instructions: instructions.0,
|
||||
suppress_model_change_announcements,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,12 +121,13 @@ impl WorldStateSection for ModelInstructionsState {
|
||||
let instructions_changed = instructions_hash.is_some_and(|current| {
|
||||
previous_instructions_hash.map_or(model_changed, |previous| previous != current)
|
||||
});
|
||||
if instructions_hash.is_some() && !model_changed && !instructions_changed {
|
||||
return (None, Vec::new());
|
||||
let announce_model = model_changed && !self.suppress_model_change_announcements;
|
||||
if instructions_hash.is_some() && !announce_model && !instructions_changed {
|
||||
return (model_changed.then(|| self.snapshot.clone()), Vec::new());
|
||||
}
|
||||
let fragment: Option<Box<dyn ContextualUserFragment>> = if instructions_hash.is_some() {
|
||||
Some(Box::new(ModelInstructions {
|
||||
model: model_changed.then(|| model.clone()),
|
||||
model: announce_model.then(|| model.clone()),
|
||||
instructions: instructions_changed.then(|| self.instructions.clone()),
|
||||
}))
|
||||
} else {
|
||||
|
||||
@@ -51,12 +51,20 @@ fn unchanged_model_does_not_render() {
|
||||
#[test]
|
||||
fn legacy_model_snapshot_keeps_its_window_until_model_change_or_compaction() {
|
||||
let legacy: ModelInstructionsSnapshot = serde_json::from_str("\"gpt-old\"").unwrap();
|
||||
let same_model = ModelInstructionsState::for_incremental_tools("gpt-old", "new prompt".into());
|
||||
let same_model = ModelInstructionsState::for_incremental_tools(
|
||||
"gpt-old",
|
||||
"new prompt".into(),
|
||||
/*suppress_model_change_announcements*/ false,
|
||||
);
|
||||
let (snapshot, updates) = same_model.render_diff(PreviousSectionState::Known(&legacy));
|
||||
assert!(snapshot.is_none());
|
||||
assert!(updates.is_empty());
|
||||
|
||||
let new_model = ModelInstructionsState::for_incremental_tools("gpt-new", "new prompt".into());
|
||||
let new_model = ModelInstructionsState::for_incremental_tools(
|
||||
"gpt-new",
|
||||
"new prompt".into(),
|
||||
/*suppress_model_change_announcements*/ false,
|
||||
);
|
||||
let (snapshot, updates) = new_model.render_diff(PreviousSectionState::Known(&legacy));
|
||||
assert!(snapshot.is_some());
|
||||
assert_eq!(updates.len(), 1);
|
||||
@@ -78,3 +86,26 @@ fn legacy_model_snapshot_keeps_its_window_until_model_change_or_compaction() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suppressed_model_changes_advance_snapshot_without_repeated_updates() {
|
||||
let initial = ModelInstructionsState::for_incremental_tools(
|
||||
"gpt-old",
|
||||
"instructions".into(),
|
||||
/*suppress_model_change_announcements*/ true,
|
||||
);
|
||||
let (snapshot, _) = initial.render_diff(PreviousSectionState::Absent);
|
||||
let previous = snapshot.unwrap();
|
||||
let switched = ModelInstructionsState::for_incremental_tools(
|
||||
"gpt-new",
|
||||
"instructions".into(),
|
||||
/*suppress_model_change_announcements*/ true,
|
||||
);
|
||||
let (snapshot, updates) = switched.render_diff(PreviousSectionState::Known(&previous));
|
||||
assert!(updates.is_empty());
|
||||
let snapshot = snapshot.unwrap();
|
||||
assert_eq!(snapshot, switched.snapshot);
|
||||
let (unchanged, updates) = switched.render_diff(PreviousSectionState::Known(&snapshot));
|
||||
assert!(unchanged.is_none());
|
||||
assert!(updates.is_empty());
|
||||
}
|
||||
|
||||
@@ -100,7 +100,14 @@ impl Session {
|
||||
Some(BaseInstructionsProvenance::Model { .. }) => render_current_instructions(),
|
||||
Some(BaseInstructionsProvenance::Custom) | None => base_instructions.text,
|
||||
};
|
||||
ModelInstructionsState::for_incremental_tools(&model_info.slug, instructions)
|
||||
ModelInstructionsState::for_incremental_tools(
|
||||
&model_info.slug,
|
||||
instructions,
|
||||
turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::SuppressModelChangeAnnouncements),
|
||||
)
|
||||
} else {
|
||||
let model_instructions = render_current_instructions();
|
||||
let base_instructions = self.get_prompt_base_instructions().await.text;
|
||||
|
||||
@@ -148,11 +148,13 @@ async fn prediction_fork_keeps_inherited_tools() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(true; "responses_lite")]
|
||||
#[test_case(false; "responses_api")]
|
||||
#[test_case(true, None; "responses_lite")]
|
||||
#[test_case(true, Some(true); "announcements_suppressed")]
|
||||
#[test_case(false, None; "responses_api")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
use_responses_lite: bool,
|
||||
suppress_model_change_announcements: Option<bool>,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -199,6 +201,12 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
}
|
||||
})
|
||||
.with_config(move |config| {
|
||||
if let Some(suppress) = suppress_model_change_announcements {
|
||||
config
|
||||
.features
|
||||
.set_enabled(Feature::SuppressModelChangeAnnouncements, suppress)
|
||||
.expect("configure model announcements");
|
||||
}
|
||||
if !use_responses_lite {
|
||||
config.base_instructions =
|
||||
Some("Use the available tools to help the user.".to_string());
|
||||
@@ -368,13 +376,17 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
);
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
if use_responses_lite {
|
||||
if suppress_model_change_announcements == Some(true) {
|
||||
"incremental_tools_suppressed_announcements"
|
||||
} else if use_responses_lite {
|
||||
"incremental_tools"
|
||||
} else {
|
||||
"incremental_tools_responses_api"
|
||||
},
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
if use_responses_lite {
|
||||
if suppress_model_change_announcements == Some(true) {
|
||||
"A model switch suppresses the model ID announcement while appending changed tools and replacement instructions. Repeated turns preserve the prefix; compaction starts a new window with the updated base."
|
||||
} else if use_responses_lite {
|
||||
"Tool definitions precede the initial base instructions. A model switch appends one developer notice, changed tools, and replacement instructions without rewriting the prefix; compaction starts a new window with the updated base."
|
||||
} else {
|
||||
"Regular Responses requests carry the full current tool catalog without an incremental notice; changed execution instructions replace the catalog on subsequent requests."
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios_incremental_tools.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(if\nsuppress_model_change_announcements == Some(true)\n{\n \"A model switch suppresses the model ID announcement while appending changed tools and replacement instructions. Repeated turns preserve the prefix; compaction starts a new window with the updated base.\"\n} else if use_responses_lite\n{\n \"Tool definitions precede the initial base instructions. A model switch appends one developer notice, changed tools, and replacement instructions without rewriting the prefix; compaction starts a new window with the updated base.\"\n} else\n{\n \"Regular Responses requests carry the full current tool catalog without an incremental notice; changed execution instructions replace the catalog on subsequent requests.\"\n}, &requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: A model switch suppresses the model ID announcement while appending changed tools and replacement instructions. Repeated turns preserve the prefix; compaction starts a new window with the updated base.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "gpt-5.4"
|
||||
parallel_tool_calls: false
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"context":"all_turns","effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
-- request 1 (turn) --
|
||||
00:additional_tools/developer (2; hash=49f981f03ae5c5ce):
|
||||
- namespace/functions; hash=cc6e8cf0e5405f3a
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/exec_command
|
||||
- function/write_stdin
|
||||
- function/request_user_input
|
||||
- custom/apply_patch
|
||||
- function/view_image
|
||||
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=400cd6ac61d524a9
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/developer:
|
||||
<PERMISSIONS_INSTRUCTIONS>
|
||||
03:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
04:message/user:
|
||||
Begin work.
|
||||
-- request 2 (turn) --
|
||||
05:message/user:
|
||||
Continue with the same tools.
|
||||
|
||||
## Window 2 (after request 2)
|
||||
Settings: relative to window 1
|
||||
model: "updated-tools-model"
|
||||
-- request 3 (turn) --
|
||||
00:additional_tools/developer (2; hash=49f981f03ae5c5ce):
|
||||
- namespace/functions; hash=cc6e8cf0e5405f3a
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/exec_command
|
||||
- function/write_stdin
|
||||
- function/request_user_input
|
||||
- custom/apply_patch
|
||||
- function/view_image
|
||||
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=400cd6ac61d524a9
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/developer:
|
||||
<PERMISSIONS_INSTRUCTIONS>
|
||||
03:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
04:message/user:
|
||||
Begin work.
|
||||
05:message/user:
|
||||
Continue with the same tools.
|
||||
06:message/developer:
|
||||
This is an incremental tools update. Previously declared tools remain available for direct calls unless ...here, its latest definition replaces the earlier one. [hash=bceb65fad1afdf9c]
|
||||
07:additional_tools/developer (1; hash=26cd263a29587cc7):
|
||||
- namespace/functions; hash=771cad2ff250d2f3
|
||||
- custom/exec
|
||||
08:message/developer:
|
||||
<model_instructions>
|
||||
The following instructions replace the previous base instructions:
|
||||
|
||||
Review results before responding.
|
||||
</model_instructions>
|
||||
09:message/user:
|
||||
Continue with the updated execution instructions.
|
||||
-- request 4 (turn) --
|
||||
10:message/user:
|
||||
Continue with the same updated tools.
|
||||
-- request 5 (compaction) --
|
||||
11:compaction_trigger
|
||||
|
||||
## Window 3 (after request 5: input diverged at item 00)
|
||||
Settings: same as window 2
|
||||
-- request 6 (turn) --
|
||||
00:additional_tools/developer (2; hash=1322c3a339378659):
|
||||
- namespace/functions; hash=9d5d7a0adab05fef
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/exec_command
|
||||
- function/write_stdin
|
||||
- function/request_user_input
|
||||
- custom/apply_patch
|
||||
- function/view_image
|
||||
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=400cd6ac61d524a9
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/user:
|
||||
Begin work.
|
||||
03:message/user:
|
||||
Continue with the same tools.
|
||||
04:message/user:
|
||||
Continue with the updated execution instructions.
|
||||
05:message/developer:
|
||||
<PERMISSIONS_INSTRUCTIONS>
|
||||
06:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
07:message/user:
|
||||
Continue with the same updated tools.
|
||||
08:compaction:encrypted=true; chars=28; hash=e4a73156e0a498bc
|
||||
09:message/user:
|
||||
Continue after compaction.
|
||||
@@ -295,6 +295,8 @@ pub enum Feature {
|
||||
DeferredToolWorldState,
|
||||
/// Track top-level tool definitions in world state and emit incremental context updates.
|
||||
IncrementalTools,
|
||||
/// Suppress the model ID announcement in incremental context after a model switch.
|
||||
SuppressModelChangeAnnouncements,
|
||||
/// Preserve inherited tool declarations in MCP-disabled prediction forks.
|
||||
PredictionPreserveParentTools,
|
||||
/// Diff CodeModeOnly exec definitions within the incremental tool catalog.
|
||||
@@ -1743,6 +1745,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::SuppressModelChangeAnnouncements,
|
||||
key: "suppress_model_change_announcements",
|
||||
stage: Stage::Stable,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::PredictionPreserveParentTools,
|
||||
key: "prediction_preserve_parent_tools",
|
||||
|
||||
@@ -979,3 +979,21 @@ fn retired_worktree_and_voice_config_overrides_are_ignored() {
|
||||
assert_eq!(features, Features::with_defaults());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_change_suppression_config_preserves_omission_and_explicit_values() {
|
||||
for (input, expected) in [
|
||||
("", false),
|
||||
("suppress_model_change_announcements = false", false),
|
||||
("suppress_model_change_announcements = true", true),
|
||||
] {
|
||||
let config: FeaturesToml = toml::from_str(input).unwrap();
|
||||
let mut features = Features::with_defaults();
|
||||
features.apply_toml(&config);
|
||||
assert_eq!(
|
||||
features.enabled(Feature::SuppressModelChangeAnnouncements),
|
||||
expected,
|
||||
"{input}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user