mirror of
https://github.com/openai/codex.git
synced 2026-04-28 08:34:54 +00:00
feat(core) ModelInfo.model_instructions_template (#9597)
## Summary #9555 is the start of a rename, so I'm starting to standardize here. Sets up `model_instructions` templating with a strongly-typed object for injecting a personality block into the model instructions. ## Testing - [x] Added tests - [x] Ran locally
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -7,10 +8,14 @@ use serde::Serialize;
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::Display;
|
||||
use strum_macros::EnumIter;
|
||||
use tracing::warn;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::config_types::Personality;
|
||||
use crate::config_types::Verbosity;
|
||||
|
||||
const PERSONALITY_PLACEHOLDER: &str = "{{ personality_message }}";
|
||||
|
||||
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -180,6 +185,8 @@ pub struct ModelInfo {
|
||||
pub priority: i32,
|
||||
pub upgrade: Option<ModelInfoUpgrade>,
|
||||
pub base_instructions: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model_instructions_template: Option<ModelInstructionsTemplate>,
|
||||
pub supports_reasoning_summaries: bool,
|
||||
pub support_verbosity: bool,
|
||||
pub default_verbosity: Option<Verbosity>,
|
||||
@@ -206,8 +213,49 @@ impl ModelInfo {
|
||||
.map(|context_window| (context_window * 9) / 10)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_model_instructions(&self, personality: Option<Personality>) -> String {
|
||||
if let Some(personality) = personality
|
||||
&& let Some(template) = &self.model_instructions_template
|
||||
&& template.has_personality_placeholder()
|
||||
&& let Some(personality_messages) = &template.personality_messages
|
||||
&& let Some(personality_message) = personality_messages.0.get(&personality)
|
||||
{
|
||||
template
|
||||
.template
|
||||
.replace(PERSONALITY_PLACEHOLDER, personality_message.as_str())
|
||||
} else if let Some(personality) = personality {
|
||||
warn!(
|
||||
model = %self.slug,
|
||||
%personality,
|
||||
"Model personality requested but model_instructions_template is invalid, falling back to base instructions."
|
||||
);
|
||||
self.base_instructions.clone()
|
||||
} else {
|
||||
self.base_instructions.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A strongly-typed template for assembling model instructions. If populated and valid, will override
|
||||
/// base_instructions.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct ModelInstructionsTemplate {
|
||||
pub template: String,
|
||||
pub personality_messages: Option<PersonalityMessages>,
|
||||
}
|
||||
|
||||
impl ModelInstructionsTemplate {
|
||||
fn has_personality_placeholder(&self) -> bool {
|
||||
self.template.contains(PERSONALITY_PLACEHOLDER)
|
||||
}
|
||||
}
|
||||
|
||||
// serializes as a dictionary from personality to message
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
pub struct PersonalityMessages(pub BTreeMap<Personality, String>);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct ModelInfoUpgrade {
|
||||
pub model: String,
|
||||
@@ -335,3 +383,67 @@ fn nearest_effort(target: ReasoningEffort, supported: &[ReasoningEffort]) -> Rea
|
||||
.min_by_key(|candidate| (effort_rank(*candidate) - target_rank).abs())
|
||||
.unwrap_or(target)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn test_model(template: Option<ModelInstructionsTemplate>) -> ModelInfo {
|
||||
ModelInfo {
|
||||
slug: "test-model".to_string(),
|
||||
display_name: "Test Model".to_string(),
|
||||
description: None,
|
||||
default_reasoning_level: None,
|
||||
supported_reasoning_levels: vec![],
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
upgrade: None,
|
||||
base_instructions: "base".to_string(),
|
||||
model_instructions_template: template,
|
||||
supports_reasoning_summaries: false,
|
||||
support_verbosity: false,
|
||||
default_verbosity: None,
|
||||
apply_patch_tool_type: None,
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
supports_parallel_tool_calls: false,
|
||||
context_window: None,
|
||||
auto_compact_token_limit: None,
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn personality_messages() -> PersonalityMessages {
|
||||
PersonalityMessages(BTreeMap::from([(
|
||||
Personality::Friendly,
|
||||
"friendly".to_string(),
|
||||
)]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_model_instructions_uses_template_when_placeholder_present() {
|
||||
let model = test_model(Some(ModelInstructionsTemplate {
|
||||
template: "Hello {{ personality_message }}".to_string(),
|
||||
personality_messages: Some(personality_messages()),
|
||||
}));
|
||||
|
||||
let instructions = model.get_model_instructions(Some(Personality::Friendly));
|
||||
|
||||
assert_eq!(instructions, "Hello friendly");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_model_instructions_falls_back_when_placeholder_missing() {
|
||||
let model = test_model(Some(ModelInstructionsTemplate {
|
||||
template: "Hello there".to_string(),
|
||||
personality_messages: Some(personality_messages()),
|
||||
}));
|
||||
|
||||
let instructions = model.get_model_instructions(Some(Personality::Friendly));
|
||||
|
||||
assert_eq!(instructions, "base");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user