mirror of
https://github.com/openai/codex.git
synced 2026-10-16 16:58:46 +00:00
Source Guardian v2 defaults from the model catalog (#38990)
## What changed - Add optional `guardian_v2` model-message defaults for classifier instructions, review thresholds, reasoning effort, transcript selection and limits, and action and compaction token limits. - Apply those defaults when Guardian v2 samples a tool call, while preserving explicit `[features.guardianv2]` settings over catalog values. - Preserve the new catalog configuration when applying model overrides. ## Testing - Cover model-catalog serialization and model override behavior. - Verify that sampling uses catalog defaults while retaining local overrides. GitOrigin-RevId: 4ae9ec235c647acbca05a20b7c4b87a4e1331161
This commit is contained in:
@@ -42,6 +42,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}),
|
||||
include_skills_usage_instructions: false,
|
||||
include_plugin_usage_instructions: false,
|
||||
|
||||
@@ -1836,6 +1836,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
};
|
||||
|
||||
let guardian_config = build_guardian_review_session_config(
|
||||
@@ -1871,6 +1872,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
};
|
||||
|
||||
let guardian_config = build_guardian_review_session_config(
|
||||
@@ -1914,6 +1916,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
};
|
||||
|
||||
let guardian_config = build_guardian_review_session_config(
|
||||
|
||||
@@ -43,6 +43,7 @@ async fn catalog_permission_message_loaded_from_remote_models_is_sent() -> Resul
|
||||
}),
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
});
|
||||
let models_mock = mount_models_once(
|
||||
&server,
|
||||
|
||||
@@ -72,6 +72,7 @@ fn model_with_collaboration_messages(
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
});
|
||||
model_messages.collaboration_modes = Some(CollaborationModeMessages {
|
||||
default: default.map(str::to_string),
|
||||
|
||||
@@ -512,6 +512,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}),
|
||||
include_skills_usage_instructions: false,
|
||||
include_plugin_usage_instructions: false,
|
||||
|
||||
@@ -60,6 +60,7 @@ fn model_with_approval_messages(
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
});
|
||||
model
|
||||
}
|
||||
@@ -78,6 +79,7 @@ fn model_with_permission_messages(
|
||||
permissions: Some(permissions),
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
});
|
||||
model
|
||||
}
|
||||
@@ -231,6 +233,7 @@ async fn catalog_non_on_request_approval_messages_are_sent_in_initial_permission
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
});
|
||||
let mut builder = test_codex()
|
||||
.with_model(model_slug)
|
||||
|
||||
@@ -645,6 +645,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}),
|
||||
include_skills_usage_instructions: false,
|
||||
include_plugin_usage_instructions: false,
|
||||
@@ -770,6 +771,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}),
|
||||
include_skills_usage_instructions: false,
|
||||
include_plugin_usage_instructions: false,
|
||||
|
||||
@@ -112,6 +112,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}),
|
||||
..specific
|
||||
};
|
||||
@@ -126,6 +127,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}),
|
||||
..generic
|
||||
};
|
||||
@@ -774,6 +776,7 @@ async fn remote_models_apply_legacy_instructions() -> Result<()> {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}),
|
||||
include_skills_usage_instructions: false,
|
||||
include_plugin_usage_instructions: false,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use codex_core::config::Config;
|
||||
use codex_features::FeatureToml;
|
||||
use codex_features::GuardianV2ConfigToml;
|
||||
use codex_features::GuardianV2TranscriptConfigToml;
|
||||
use codex_protocol::openai_models::GuardianV2ModelConfig;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
|
||||
use crate::transcript::MAX_MESSAGE_ENTRY_TOKENS;
|
||||
@@ -24,6 +26,7 @@ Return an action_risk score from 0.0 (safe and authorized) to 1.0 (dangerous or
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct GuardianV2Config {
|
||||
local_overrides: GuardianV2ConfigToml,
|
||||
pub(crate) classifier_instructions: String,
|
||||
pub(crate) review_threshold: f64,
|
||||
pub(crate) reasoning_effort: ReasoningEffort,
|
||||
@@ -53,6 +56,81 @@ impl GuardianV2Config {
|
||||
None => GuardianV2ConfigToml::default(),
|
||||
};
|
||||
|
||||
Self::from_overrides(configured)
|
||||
}
|
||||
|
||||
pub(crate) fn with_model_defaults(
|
||||
&self,
|
||||
model_defaults: Option<&GuardianV2ModelConfig>,
|
||||
) -> Result<Self, String> {
|
||||
let mut configured = self.local_overrides.clone();
|
||||
if let Some(model_defaults) = model_defaults {
|
||||
configured.classifier_instructions = configured
|
||||
.classifier_instructions
|
||||
.or_else(|| model_defaults.classifier_instructions.clone());
|
||||
configured.review_threshold = configured.review_threshold.or_else(|| {
|
||||
model_defaults
|
||||
.review_threshold_basis_points
|
||||
.map(|basis_points| f64::from(basis_points) / 10_000.0)
|
||||
});
|
||||
configured.reasoning_effort = configured
|
||||
.reasoning_effort
|
||||
.or_else(|| model_defaults.reasoning_effort.clone());
|
||||
configured.max_action_tokens = configured
|
||||
.max_action_tokens
|
||||
.or(model_defaults.max_action_tokens);
|
||||
configured.max_classifier_instruction_tokens = configured
|
||||
.max_classifier_instruction_tokens
|
||||
.or(model_defaults.max_classifier_instruction_tokens);
|
||||
configured.max_parent_compaction_tokens = configured
|
||||
.max_parent_compaction_tokens
|
||||
.or(model_defaults.max_parent_compaction_tokens);
|
||||
|
||||
if let Some(model_transcript) = model_defaults.transcript.as_ref() {
|
||||
let transcript = configured
|
||||
.transcript
|
||||
.get_or_insert_with(GuardianV2TranscriptConfigToml::default);
|
||||
if transcript.sources.is_none()
|
||||
&& let Some(sources) = model_transcript.sources.as_ref()
|
||||
{
|
||||
transcript.sources = Some(
|
||||
sources
|
||||
.iter()
|
||||
.map(|source| match source.as_str() {
|
||||
"tool_calls" => Ok(TranscriptSource::ToolCalls),
|
||||
"tool_outputs" => Ok(TranscriptSource::ToolOutputs),
|
||||
"reasoning" => Ok(TranscriptSource::Reasoning),
|
||||
_ => {
|
||||
Err(format!("invalid Guardian v2 transcript source: {source}"))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
transcript.max_message_entry_tokens = transcript
|
||||
.max_message_entry_tokens
|
||||
.or(model_transcript.max_message_entry_tokens);
|
||||
transcript.max_tool_entry_tokens = transcript
|
||||
.max_tool_entry_tokens
|
||||
.or(model_transcript.max_tool_entry_tokens);
|
||||
transcript.max_message_transcript_tokens = transcript
|
||||
.max_message_transcript_tokens
|
||||
.or(model_transcript.max_message_transcript_tokens);
|
||||
transcript.max_tool_transcript_tokens = transcript
|
||||
.max_tool_transcript_tokens
|
||||
.or(model_transcript.max_tool_transcript_tokens);
|
||||
transcript.max_recent_non_user_entries = transcript
|
||||
.max_recent_non_user_entries
|
||||
.or(model_transcript.max_recent_non_user_entries);
|
||||
}
|
||||
}
|
||||
|
||||
let mut resolved = Self::from_overrides(configured)?;
|
||||
resolved.local_overrides = self.local_overrides.clone();
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn from_overrides(configured: GuardianV2ConfigToml) -> Result<Self, String> {
|
||||
let review_threshold = configured
|
||||
.review_threshold
|
||||
.unwrap_or(DEFAULT_REVIEW_THRESHOLD);
|
||||
@@ -126,6 +204,7 @@ impl GuardianV2Config {
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
local_overrides: configured.clone(),
|
||||
classifier_instructions: truncate_entry(
|
||||
configured
|
||||
.classifier_instructions
|
||||
|
||||
@@ -288,6 +288,23 @@ impl ToolLifecycleContributor for GuardianV2Extension {
|
||||
let Some(guardian_config) = input.thread_store.get::<GuardianV2Config>() else {
|
||||
return Box::pin(std::future::ready(()));
|
||||
};
|
||||
let parent_model = input.thread_store.get::<ModelInfo>();
|
||||
let model_defaults = parent_model
|
||||
.as_ref()
|
||||
.and_then(|model| model.model_messages.as_ref())
|
||||
.and_then(|messages| messages.guardian_v2.as_ref());
|
||||
let guardian_config = match guardian_config.with_model_defaults(model_defaults) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
self.event_sink.emit_warning(ExtensionWarning {
|
||||
thread_id: input.thread_store.level_id().to_owned(),
|
||||
turn_id: Some(input.turn_id.to_owned()),
|
||||
message: error,
|
||||
});
|
||||
return Box::pin(std::future::ready(()));
|
||||
}
|
||||
};
|
||||
input.thread_store.insert(guardian_config.clone());
|
||||
let sampled_at = SystemTime::now();
|
||||
let latest_parent_compaction = input
|
||||
.conversation_history
|
||||
@@ -337,7 +354,6 @@ impl ToolLifecycleContributor for GuardianV2Extension {
|
||||
tool_name: input.tool_name.clone(),
|
||||
payload: input.payload.clone(),
|
||||
};
|
||||
let parent_model = input.thread_store.get::<ModelInfo>();
|
||||
let parent_compaction_hash = parent_model
|
||||
.as_ref()
|
||||
.and_then(|model| model.comp_hash.clone());
|
||||
|
||||
@@ -26,6 +26,9 @@ use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ImageDetail;
|
||||
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use codex_protocol::openai_models::GuardianV2ModelConfig;
|
||||
use codex_protocol::openai_models::GuardianV2TranscriptModelConfig;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::TruncationPolicy;
|
||||
@@ -222,8 +225,14 @@ async fn sample_conversation_history(
|
||||
arguments: &str,
|
||||
guardian_policy: Option<&str>,
|
||||
) -> Result<(serde_json::Value, TestCodex, ExtensionRegistry<Config>)> {
|
||||
sample_configured_conversation_history(conversation_history, arguments, guardian_policy, "")
|
||||
.await
|
||||
sample_configured_conversation_history(
|
||||
conversation_history,
|
||||
arguments,
|
||||
guardian_policy,
|
||||
"",
|
||||
/*model_defaults*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn sample_configured_conversation_history(
|
||||
@@ -231,11 +240,13 @@ async fn sample_configured_conversation_history(
|
||||
arguments: &str,
|
||||
guardian_policy: Option<&str>,
|
||||
guardian_config: &str,
|
||||
model_defaults: Option<GuardianV2ModelConfig>,
|
||||
) -> Result<(serde_json::Value, TestCodex, ExtensionRegistry<Config>)> {
|
||||
let thread_server = responses::start_mock_server().await;
|
||||
let guardian_policy = guardian_policy.map(str::to_owned);
|
||||
let guardian_config = guardian_config.to_owned();
|
||||
let test = test_codex()
|
||||
let has_model_defaults = model_defaults.is_some();
|
||||
let builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_model_info_override("codex-auto-review", |model_info| {
|
||||
model_info
|
||||
@@ -254,9 +265,19 @@ async fn sample_configured_conversation_history(
|
||||
std::fs::write(home.join("config.toml"), guardian_config)
|
||||
.expect("Guardian v2 configuration should be written");
|
||||
}
|
||||
});
|
||||
let mut builder = if let Some(model_defaults) = model_defaults {
|
||||
builder.with_model_info_override("gpt-5.5", move |model| {
|
||||
model
|
||||
.model_messages
|
||||
.as_mut()
|
||||
.expect("test model should expose model messages")
|
||||
.guardian_v2 = Some(model_defaults);
|
||||
})
|
||||
.build_with_auto_env(&thread_server)
|
||||
.await?;
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
let test = builder.build_with_auto_env(&thread_server).await?;
|
||||
let events = vec![
|
||||
ev_assistant_message("sample", r#"{"scores":{"action_risk":0.8}}"#),
|
||||
ev_completed("response-1"),
|
||||
@@ -279,6 +300,14 @@ async fn sample_configured_conversation_history(
|
||||
let registry = builder.build();
|
||||
let session_store = ExtensionData::new("session-1");
|
||||
let thread_store = test.codex.thread_extension_data();
|
||||
if has_model_defaults {
|
||||
let parent_model = test
|
||||
.thread_manager
|
||||
.get_models_manager()
|
||||
.get_model_info("gpt-5.5", &config.to_models_manager_config())
|
||||
.await;
|
||||
thread_store.insert(parent_model);
|
||||
}
|
||||
assert_eq!(
|
||||
registry
|
||||
.approval_review(&session_store, thread_store, "review action")
|
||||
@@ -389,6 +418,7 @@ max_recent_non_user_entries = 8
|
||||
&arguments,
|
||||
Some(TEST_GUARDIAN_POLICY),
|
||||
configuration,
|
||||
/*model_defaults*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -508,6 +538,7 @@ include_images = true
|
||||
r#"{"path":"README.md"}"#,
|
||||
Some(TEST_GUARDIAN_POLICY),
|
||||
configuration,
|
||||
/*model_defaults*/ None,
|
||||
)
|
||||
.await?;
|
||||
let content = request["input"][2]["content"]
|
||||
@@ -531,6 +562,121 @@ include_images = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn contributor_uses_model_defaults_and_preserves_local_overrides() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let model_defaults = GuardianV2ModelConfig {
|
||||
classifier_instructions: Some("Use the experimental model-owned prompt.".to_owned()),
|
||||
review_threshold_basis_points: Some(6_000),
|
||||
reasoning_effort: Some(ReasoningEffort::Minimal),
|
||||
transcript: Some(GuardianV2TranscriptModelConfig {
|
||||
sources: Some(vec!["reasoning".to_owned()]),
|
||||
max_message_entry_tokens: Some(128),
|
||||
max_message_transcript_tokens: Some(256),
|
||||
..Default::default()
|
||||
}),
|
||||
max_action_tokens: Some(128),
|
||||
max_classifier_instruction_tokens: Some(256),
|
||||
max_parent_compaction_tokens: Some(384),
|
||||
};
|
||||
let conversation_history = vec![
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_owned(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Review the pending action.".to_owned(),
|
||||
}],
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
ResponseItem::Reasoning {
|
||||
id: None,
|
||||
summary: vec![ReasoningItemReasoningSummary::SummaryText {
|
||||
text: "Use the experimental transcript.".to_owned(),
|
||||
}],
|
||||
content: None,
|
||||
encrypted_content: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "list_dir".to_owned(),
|
||||
namespace: None,
|
||||
arguments: r#"{"path":"."}"#.to_owned(),
|
||||
encrypted_function_args: None,
|
||||
call_id: "previous-call".to_owned(),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
];
|
||||
let arguments = json!({"body": "x".repeat(4_000)}).to_string();
|
||||
let local_config = "[features.guardianv2]\nenabled = true\nreview_threshold = 0.70\n";
|
||||
let (request, test, registry) = sample_configured_conversation_history(
|
||||
conversation_history,
|
||||
&arguments,
|
||||
Some(TEST_GUARDIAN_POLICY),
|
||||
local_config,
|
||||
Some(model_defaults),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
request["input"][1],
|
||||
json!({
|
||||
"type": "message",
|
||||
"role": "developer",
|
||||
"content": [{
|
||||
"type": "input_text",
|
||||
"text": format!(
|
||||
"Use the experimental model-owned prompt.\n\n# Security Policy\n{TEST_GUARDIAN_POLICY}"
|
||||
)
|
||||
}]
|
||||
})
|
||||
);
|
||||
assert_eq!(request["reasoning"]["effort"], "minimal");
|
||||
let content = request["input"][2]["content"]
|
||||
.as_array()
|
||||
.expect("Luna user content should be an array");
|
||||
let transcript = content
|
||||
.iter()
|
||||
.filter_map(|item| item["text"].as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(transcript.contains(&"[2] reasoning: Use the experimental transcript.\n"));
|
||||
assert!(!transcript.iter().any(|entry| entry.contains("list_dir")));
|
||||
let action = content[content.len() - 2]["text"]
|
||||
.as_str()
|
||||
.expect("planned action should be a text item");
|
||||
assert!(action.len() <= TruncationPolicy::Tokens(/*limit*/ 128).byte_budget());
|
||||
|
||||
let session_store = ExtensionData::new("session-1");
|
||||
let thread_store = test.codex.thread_extension_data();
|
||||
assert_eq!(
|
||||
thread_store
|
||||
.get::<crate::config::GuardianV2Config>()
|
||||
.expect("Guardian v2 configuration should be installed")
|
||||
.max_parent_compaction_tokens,
|
||||
384
|
||||
);
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
while thread_store.get::<SecurityRiskScore>().is_none() {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
thread_store.insert(SecurityRiskScore {
|
||||
scores: BTreeMap::from([("action_risk".to_owned(), 0.65)]),
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
registry
|
||||
.approval_review(&session_store, thread_store, "review action")
|
||||
.await,
|
||||
Some(ReviewDecision::Approved)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -59,6 +59,7 @@ pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig)
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
});
|
||||
model_messages.instructions_template = Some(base_instructions.clone());
|
||||
model_messages.instructions_variables = None;
|
||||
@@ -201,6 +202,7 @@ fn local_model_messages_for_slug(slug: &str) -> ModelMessages {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
},
|
||||
_ => ModelMessages {
|
||||
instructions_template: Some(BASE_INSTRUCTIONS.to_string()),
|
||||
@@ -211,6 +213,7 @@ fn local_model_messages_for_slug(slug: &str) -> ModelMessages {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::openai_models::ApprovalMessages;
|
||||
use codex_protocol::openai_models::AutoReviewMessages;
|
||||
use codex_protocol::openai_models::CollaborationModeMessages;
|
||||
use codex_protocol::openai_models::GuardianV2ModelConfig;
|
||||
use codex_protocol::openai_models::ModelTokenBudgetConfig;
|
||||
use codex_protocol::openai_models::MultiAgentMessages;
|
||||
use codex_protocol::openai_models::MultiAgentModeMessages;
|
||||
@@ -59,6 +60,10 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
|
||||
auto_compact_fallback_prompt: "compact prompt".to_string(),
|
||||
auto_compact_fallback_buffer_tokens: 64,
|
||||
};
|
||||
let guardian_v2 = GuardianV2ModelConfig {
|
||||
classifier_instructions: Some("Guardian experiment".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
model.model_messages = Some(ModelMessages {
|
||||
instructions_template: Some("template".to_string()),
|
||||
instructions_variables: Some(ModelInstructionsVariables {
|
||||
@@ -72,6 +77,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
|
||||
permissions: Some(permissions.clone()),
|
||||
multi_agent: Some(multi_agent.clone()),
|
||||
token_budget: Some(token_budget.clone()),
|
||||
guardian_v2: Some(guardian_v2.clone()),
|
||||
});
|
||||
let config = ModelsManagerConfig {
|
||||
base_instructions: Some(override_instructions.to_string()),
|
||||
@@ -91,6 +97,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
|
||||
permissions: Some(permissions),
|
||||
multi_agent: Some(multi_agent),
|
||||
token_budget: Some(token_budget),
|
||||
guardian_v2: Some(guardian_v2),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -121,6 +128,7 @@ fn disabled_personality_bakes_default_and_preserves_catalog_approval_messages()
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
});
|
||||
let config = ModelsManagerConfig {
|
||||
personality_enabled: false,
|
||||
@@ -140,6 +148,7 @@ fn disabled_personality_bakes_default_and_preserves_catalog_approval_messages()
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -166,6 +175,7 @@ fn disabled_personality_uses_plain_base_instructions_for_local_personality_model
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}),
|
||||
"unexpected model messages for {slug}"
|
||||
);
|
||||
@@ -206,6 +216,7 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
});
|
||||
|
||||
let updated = with_config_overrides(model, &config);
|
||||
|
||||
@@ -33,6 +33,12 @@ use crate::config_types::ServiceTier;
|
||||
use crate::config_types::Verbosity;
|
||||
use crate::protocol::MultiAgentVersion;
|
||||
|
||||
#[path = "openai_models/guardian_v2.rs"]
|
||||
mod guardian_v2;
|
||||
|
||||
pub use guardian_v2::GuardianV2ModelConfig;
|
||||
pub use guardian_v2::GuardianV2TranscriptModelConfig;
|
||||
|
||||
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
|
||||
/// Backend model-catalog specialty identifying cybersecurity-focused models.
|
||||
pub const MODEL_SPECIALTY_CYBER: &str = "cyber";
|
||||
@@ -534,6 +540,8 @@ pub struct ModelMessages {
|
||||
pub multi_agent: Option<MultiAgentMessages>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub token_budget: Option<ModelTokenBudgetConfig>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub guardian_v2: Option<GuardianV2ModelConfig>,
|
||||
}
|
||||
|
||||
/// Model-owned defaults for the context-window token-budget feature.
|
||||
@@ -758,6 +766,7 @@ where
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
});
|
||||
messages.instructions_template = Some(base_instructions);
|
||||
}
|
||||
@@ -947,6 +956,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1088,6 +1098,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1169,6 +1180,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}));
|
||||
|
||||
let instructions = model.get_model_instructions(Some(Personality::Friendly));
|
||||
@@ -1191,6 +1203,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}));
|
||||
assert_eq!(
|
||||
model.get_model_instructions(Some(Personality::Pragmatic)),
|
||||
@@ -1214,6 +1227,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}));
|
||||
assert_eq!(
|
||||
model_no_personality.get_model_instructions(Some(Personality::Friendly)),
|
||||
@@ -1248,6 +1262,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}));
|
||||
|
||||
let instructions = model.get_model_instructions(Some(Personality::Friendly));
|
||||
@@ -1288,6 +1303,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1339,6 +1355,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
}))],
|
||||
};
|
||||
|
||||
@@ -1376,6 +1393,17 @@ mod tests {
|
||||
}),
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: Some(GuardianV2ModelConfig {
|
||||
classifier_instructions: Some("Guardian classification".to_string()),
|
||||
review_threshold_basis_points: Some(7_500),
|
||||
reasoning_effort: Some(ReasoningEffort::Minimal),
|
||||
transcript: Some(GuardianV2TranscriptModelConfig {
|
||||
sources: Some(vec!["reasoning".to_string()]),
|
||||
max_tool_entry_tokens: Some(500),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
let mut value = serde_json::to_value(ModelsResponse {
|
||||
models: vec![test_model(Some(messages.clone()))],
|
||||
@@ -1398,6 +1426,7 @@ mod tests {
|
||||
permissions: None,
|
||||
multi_agent: None,
|
||||
token_budget: None,
|
||||
guardian_v2: None,
|
||||
};
|
||||
let mut value = serde_json::to_value(ModelsResponse {
|
||||
models: vec![test_model(Some(canonical_messages.clone()))],
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
use super::ReasoningEffort;
|
||||
|
||||
/// Optional model-owned defaults for Guardian v2 classification experiments.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct GuardianV2ModelConfig {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub classifier_instructions: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub review_threshold_basis_points: Option<u16>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub transcript: Option<GuardianV2TranscriptModelConfig>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_action_tokens: Option<usize>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_classifier_instruction_tokens: Option<usize>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_parent_compaction_tokens: Option<usize>,
|
||||
}
|
||||
|
||||
/// Optional model-owned defaults for selecting and bounding Guardian v2 history.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct GuardianV2TranscriptModelConfig {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sources: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_message_entry_tokens: Option<usize>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_tool_entry_tokens: Option<usize>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_message_transcript_tokens: Option<usize>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_tool_transcript_tokens: Option<usize>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_recent_non_user_entries: Option<usize>,
|
||||
}
|
||||
Reference in New Issue
Block a user