mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Support model catalog overrides for plugin guidance (#58894)
## What changed - Add `model_messages.plugins` overrides for generic usage guidance, selected-plugin hints, and recommendation introductions, with independent fallback to bundled wording for missing or invalid fields. - Validate text limits and required template placeholders while keeping plugin identities, available capabilities, and eligibility controlled by the client. - Track generic guidance changes across model switches and session resume, emitting replacement or retirement notices and supporting legacy snapshots and retained history. ## Testing Add unit and scenario coverage for sparse overrides, literal text preservation, template validation, UTF-8 size bounds, capability filtering, recommendation fallback, and guidance recovery after resume. GitOrigin-RevId: 148b658c2d8490c9d0dc208addbee1be5342a5f5
This commit is contained in:
@@ -41,6 +41,11 @@ mod memento_v2;
|
||||
pub use memento_v2::ContextManagementToolMessages;
|
||||
pub use memento_v2::HistoryToolMessages;
|
||||
pub use memento_v2::NotesToolMessages;
|
||||
mod plugins;
|
||||
pub use plugins::GenericPluginMessages;
|
||||
pub use plugins::PluginMessages;
|
||||
pub use plugins::PluginRecommendationMessages;
|
||||
pub use plugins::SelectedPluginMessages;
|
||||
#[path = "openai_models/guardian.rs"]
|
||||
mod guardian;
|
||||
pub use guardian::GuardianModelPolicy;
|
||||
@@ -583,6 +588,9 @@ pub struct ModelMessages {
|
||||
/// Optional model-owned Apps wording, transported without runtime interpretation.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub apps: Option<AppsMessages>,
|
||||
/// Model-owned plugin wording; runtime capability facts and eligibility remain client-owned.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub plugins: Option<PluginMessages>,
|
||||
/// Developer guidance after a content-filter block. Missing, null, blank, or values over
|
||||
/// 512 UTF-8 bytes use the bundled guidance.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1764,6 +1772,7 @@ mod tests {
|
||||
apps: Some(AppsMessages {
|
||||
usage_instructions: Some("Literal Apps {{guidance}}".to_string()),
|
||||
}),
|
||||
plugins: None,
|
||||
content_filter_guidance: Some("Offer a permitted alternative.".to_string()),
|
||||
persistent_instructions: Some("Persistent catalog instructions".to_string()),
|
||||
tools: Some(ToolMessages {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Sparse model-catalog wording for generic, selected, and recommended plugins.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// Sparse plugin wording families, independent of runtime availability and authorization.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct PluginMessages {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub generic: Option<GenericPluginMessages>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub selected: Option<SelectedPluginMessages>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub recommendations: Option<PluginRecommendationMessages>,
|
||||
}
|
||||
|
||||
/// Literal generic plugin wording. Missing, null, blank, or invalid values use bundled text.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct GenericPluginMessages {
|
||||
/// Literal text, up to 500 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub intro: Option<String>,
|
||||
/// Up to 2,000 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub usage_instructions: Option<String>,
|
||||
}
|
||||
|
||||
/// Selected-plugin wording. Fact-bearing fields require exactly one documented placeholder.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct SelectedPluginMessages {
|
||||
/// Up to 500 UTF-8 bytes. Selected intro requires `{{ plugin_name }}`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub intro: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub search_tools: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes; requires `{{ skill_namespace }}`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub skill_namespace: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes; requires `{{ apps }}`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub apps: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes; requires `{{ mcp_servers }}`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mcp_servers: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub closing: Option<String>,
|
||||
/// Nonblank omission notice, up to 500 UTF-8 bytes, including any desired separator.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub omitted: Option<String>,
|
||||
}
|
||||
|
||||
/// Literal recommendation introduction; plugin identities remain supplied by the harness.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct PluginRecommendationMessages {
|
||||
/// Literal text, up to 500 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub intro: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "plugins_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,34 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::openai_models::ModelMessages;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn plugin_messages_preserve_sparse_nested_and_literal_wire_values() {
|
||||
for plugins in [
|
||||
serde_json::Value::Null,
|
||||
json!({}),
|
||||
json!({"generic": {}, "selected": {}, "recommendations": {}}),
|
||||
json!({
|
||||
"generic": {"intro": "", "usage_instructions": " \r\n\t"},
|
||||
"selected": {
|
||||
"intro": " {{ plugin_name }}\r\n",
|
||||
"search_tools": "literal {{braces}}",
|
||||
"skill_namespace": "{{ skill_namespace }}",
|
||||
"apps": "{{ apps }}",
|
||||
"mcp_servers": "{{ mcp_servers }}",
|
||||
"closing": "é".repeat(251),
|
||||
"omitted": "omitted"
|
||||
},
|
||||
"recommendations": {"intro": "recommendations"}
|
||||
}),
|
||||
] {
|
||||
let messages: ModelMessages = serde_json::from_value(json!({"plugins": plugins})).unwrap();
|
||||
let mut expected = serde_json::to_value(ModelMessages::default()).unwrap();
|
||||
if !plugins.is_null() {
|
||||
expected["plugins"] = plugins;
|
||||
}
|
||||
assert_eq!(serde_json::to_value(messages).unwrap(), expected);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user