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:
felixxia-oai
2026-08-17 11:18:50 +00:00
committed by copyberry
parent 37cf6c84c0
commit 8bf50439f2
15 changed files with 347 additions and 6 deletions

View File

@@ -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()))],

View File

@@ -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>,
}