mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
Merge remote-tracking branch 'origin/main' into nornagon/codex/thread-artifacts-runtime
# Conflicts: # codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts # codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts # codex-rs/app-server/src/request_processors.rs # codex-rs/core/src/tools/spec_plan.rs # codex-rs/state/src/runtime/threads.rs # codex-rs/thread-store/src/in_memory.rs # codex-rs/thread-store/src/local/list_threads.rs # codex-rs/thread-store/src/local/mod.rs # codex-rs/tui/src/lib.rs
This commit is contained in:
@@ -2,6 +2,6 @@ load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "protocol",
|
||||
crate_name = "codex_protocol",
|
||||
compile_data = glob(["src/prompts/**/*.md"]),
|
||||
crate_name = "codex_protocol",
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ codex-execpolicy = { workspace = true }
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-image = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
codex-utils-string = { workspace = true }
|
||||
encoding_rs = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
@@ -59,4 +60,5 @@ tempfile = { workspace = true }
|
||||
[package.metadata.cargo-shear]
|
||||
# Required because:
|
||||
# `icu_provider`: contains a required `sync` feature for `icu_decimal`
|
||||
ignored = ["icu_provider"]
|
||||
# `strum`: is referenced by generated `EnumIter` derive implementations
|
||||
ignored = ["icu_provider", "strum"]
|
||||
|
||||
@@ -34,8 +34,21 @@ pub enum PlanType {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ProviderAccount {
|
||||
ApiKey,
|
||||
Chatgpt { email: String, plan_type: PlanType },
|
||||
AmazonBedrock,
|
||||
Chatgpt {
|
||||
email: String,
|
||||
plan_type: PlanType,
|
||||
},
|
||||
AmazonBedrock {
|
||||
credential_source: AmazonBedrockCredentialSource,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
pub enum AmazonBedrockCredentialSource {
|
||||
CodexManaged,
|
||||
AwsManaged,
|
||||
}
|
||||
|
||||
impl PlanType {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// A user-selected root that can expose one or more runtime capabilities.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct SelectedCapabilityRoot {
|
||||
/// Stable identifier supplied by the capability selection platform.
|
||||
pub id: String,
|
||||
/// Where the selected root can be resolved.
|
||||
pub location: CapabilityRootLocation,
|
||||
}
|
||||
|
||||
/// Location used to resolve a selected capability root.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum CapabilityRootLocation {
|
||||
/// A path owned by an execution environment.
|
||||
Environment {
|
||||
#[serde(rename = "environmentId")]
|
||||
#[ts(rename = "environmentId")]
|
||||
environment_id: String,
|
||||
path: String,
|
||||
},
|
||||
}
|
||||
@@ -166,8 +166,8 @@ pub enum ApprovalsReviewer {
|
||||
#[default]
|
||||
#[serde(rename = "user")]
|
||||
User,
|
||||
#[serde(rename = "guardian_subagent", alias = "auto_review")]
|
||||
#[strum(serialize = "guardian_subagent")]
|
||||
#[serde(rename = "auto_review", alias = "guardian_subagent")]
|
||||
#[strum(serialize = "auto_review")]
|
||||
AutoReview,
|
||||
}
|
||||
|
||||
@@ -635,7 +635,7 @@ impl CollaborationMode {
|
||||
}
|
||||
|
||||
pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
|
||||
self.settings_ref().reasoning_effort
|
||||
self.settings_ref().reasoning_effort.clone()
|
||||
}
|
||||
|
||||
/// Updates the collaboration mode with new model and/or effort values.
|
||||
@@ -654,7 +654,7 @@ impl CollaborationMode {
|
||||
let settings = self.settings_ref();
|
||||
let updated_settings = Settings {
|
||||
model: model.unwrap_or_else(|| settings.model.clone()),
|
||||
reasoning_effort: effort.unwrap_or(settings.reasoning_effort),
|
||||
reasoning_effort: effort.unwrap_or_else(|| settings.reasoning_effort.clone()),
|
||||
developer_instructions: developer_instructions
|
||||
.unwrap_or_else(|| settings.developer_instructions.clone()),
|
||||
};
|
||||
@@ -676,7 +676,10 @@ impl CollaborationMode {
|
||||
mode: mask.mode.unwrap_or(self.mode),
|
||||
settings: Settings {
|
||||
model: mask.model.clone().unwrap_or_else(|| settings.model.clone()),
|
||||
reasoning_effort: mask.reasoning_effort.unwrap_or(settings.reasoning_effort),
|
||||
reasoning_effort: mask
|
||||
.reasoning_effort
|
||||
.clone()
|
||||
.unwrap_or_else(|| settings.reasoning_effort.clone()),
|
||||
developer_instructions: mask
|
||||
.developer_instructions
|
||||
.clone()
|
||||
@@ -757,7 +760,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&ApprovalsReviewer::AutoReview).expect("serialize reviewer"),
|
||||
"\"guardian_subagent\""
|
||||
"\"auto_review\""
|
||||
);
|
||||
|
||||
for value in ["user", "auto_review", "guardian_subagent"] {
|
||||
|
||||
@@ -2,21 +2,46 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::de::Error as _;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type", export_to = "v2/")]
|
||||
pub enum DynamicToolSpec {
|
||||
Function(DynamicToolFunctionSpec),
|
||||
Namespace(DynamicToolNamespaceSpec),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamicToolSpec {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub namespace: Option<String>,
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DynamicToolFunctionSpec {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: JsonValue,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub defer_loading: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DynamicToolNamespaceSpec {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub tools: Vec<DynamicToolNamespaceTool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type", export_to = "v2/")]
|
||||
pub enum DynamicToolNamespaceTool {
|
||||
Function(DynamicToolFunctionSpec),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamicToolCallRequest {
|
||||
@@ -47,9 +72,11 @@ pub enum DynamicToolCallOutputContentItem {
|
||||
InputImage { image_url: String },
|
||||
}
|
||||
|
||||
/// Former flat `SessionMeta` shape, including the old `exposeToContext` flag.
|
||||
/// Kept so new builds can resume sessions written before explicit namespaces.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DynamicToolSpecDe {
|
||||
struct LegacyDynamicToolSpec {
|
||||
namespace: Option<String>,
|
||||
name: String,
|
||||
description: String,
|
||||
@@ -58,84 +85,89 @@ struct DynamicToolSpecDe {
|
||||
expose_to_context: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let DynamicToolSpecDe {
|
||||
namespace,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
defer_loading,
|
||||
expose_to_context,
|
||||
} = DynamicToolSpecDe::deserialize(deserializer)?;
|
||||
|
||||
Ok(Self {
|
||||
namespace,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
defer_loading: defer_loading
|
||||
.unwrap_or_else(|| expose_to_context.map(|visible| !visible).unwrap_or(false)),
|
||||
})
|
||||
pub fn normalize_dynamic_tool_specs(
|
||||
values: Vec<JsonValue>,
|
||||
) -> Result<Vec<DynamicToolSpec>, serde_json::Error> {
|
||||
let has_legacy_fields = |value: &JsonValue| {
|
||||
value.get("namespace").is_some()
|
||||
|| value.get("exposeToContext").is_some()
|
||||
|| value.get("type").is_none()
|
||||
};
|
||||
let has_legacy_format = values.iter().any(|value| {
|
||||
has_legacy_fields(value)
|
||||
|| value
|
||||
.get("tools")
|
||||
.and_then(JsonValue::as_array)
|
||||
.is_some_and(|tools| tools.iter().any(&has_legacy_fields))
|
||||
});
|
||||
let has_canonical_format = values.iter().any(|value| value.get("type").is_some());
|
||||
if has_legacy_format && has_canonical_format {
|
||||
return Err(serde_json::Error::custom(
|
||||
"dynamic tools must use either canonical or legacy format consistently",
|
||||
));
|
||||
}
|
||||
if !has_legacy_format {
|
||||
return values.into_iter().map(serde_json::from_value).collect();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DynamicToolSpec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn dynamic_tool_spec_deserializes_defer_loading() {
|
||||
let value = json!({
|
||||
"name": "lookup_ticket",
|
||||
"description": "Fetch a ticket",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"deferLoading": true,
|
||||
});
|
||||
|
||||
let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize");
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" }
|
||||
}
|
||||
let tools = values
|
||||
.into_iter()
|
||||
.map(|value| {
|
||||
let tool: LegacyDynamicToolSpec = serde_json::from_value(value)?;
|
||||
let function = DynamicToolFunctionSpec {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.input_schema,
|
||||
defer_loading: tool.defer_loading.unwrap_or_else(|| {
|
||||
tool.expose_to_context
|
||||
.map(|visible| !visible)
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
defer_loading: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_tool_spec_legacy_expose_to_context_inverts_to_defer_loading() {
|
||||
let value = json!({
|
||||
"name": "lookup_ticket",
|
||||
"description": "Fetch a ticket",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"exposeToContext": false,
|
||||
});
|
||||
|
||||
let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize");
|
||||
|
||||
assert!(actual.defer_loading);
|
||||
}
|
||||
};
|
||||
Ok((tool.namespace, function))
|
||||
})
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?;
|
||||
Ok(group_dynamic_tools_by_namespace(tools))
|
||||
}
|
||||
|
||||
pub fn group_dynamic_tools_by_namespace(
|
||||
tools: Vec<(Option<String>, DynamicToolFunctionSpec)>,
|
||||
) -> Vec<DynamicToolSpec> {
|
||||
let mut grouped_tools = Vec::with_capacity(tools.len());
|
||||
let mut namespace_indices = HashMap::<String, usize>::new();
|
||||
for (namespace, function) in tools {
|
||||
let Some(namespace) = namespace else {
|
||||
grouped_tools.push(DynamicToolSpec::Function(function));
|
||||
continue;
|
||||
};
|
||||
let function = DynamicToolNamespaceTool::Function(function);
|
||||
if let Some(index) = namespace_indices.get(&namespace).copied() {
|
||||
let DynamicToolSpec::Namespace(namespace) = &mut grouped_tools[index] else {
|
||||
unreachable!("namespace index must point to a namespace");
|
||||
};
|
||||
namespace.tools.push(function);
|
||||
continue;
|
||||
}
|
||||
namespace_indices.insert(namespace.clone(), grouped_tools.len());
|
||||
grouped_tools.push(DynamicToolSpec::Namespace(DynamicToolNamespaceSpec {
|
||||
name: namespace,
|
||||
description: String::new(),
|
||||
tools: vec![function],
|
||||
}));
|
||||
}
|
||||
grouped_tools
|
||||
}
|
||||
|
||||
pub fn deserialize_dynamic_tool_specs<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Vec<DynamicToolSpec>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let Some(values) = Option::<Vec<JsonValue>>::deserialize(deserializer)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
normalize_dynamic_tool_specs(values)
|
||||
.map(Some)
|
||||
.map_err(D::Error::custom)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ fn rate_limit_snapshot() -> RateLimitSnapshot {
|
||||
resets_at: Some(secondary_reset_at),
|
||||
}),
|
||||
credits: None,
|
||||
individual_limit: None,
|
||||
plan_type: None,
|
||||
rate_limit_reached_type: None,
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ pub enum TurnItem {
|
||||
Reasoning(ReasoningItem),
|
||||
WebSearch(WebSearchItem),
|
||||
ImageView(ImageViewItem),
|
||||
Sleep(SleepItem),
|
||||
ImageGeneration(ImageGenerationItem),
|
||||
FileChange(FileChangeItem),
|
||||
McpToolCall(McpToolCallItem),
|
||||
@@ -140,6 +141,12 @@ pub struct ImageViewItem {
|
||||
pub path: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
|
||||
pub struct SleepItem {
|
||||
pub id: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
|
||||
pub struct ImageGenerationItem {
|
||||
pub id: String,
|
||||
@@ -391,6 +398,7 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<Res
|
||||
role: "user".to_string(),
|
||||
content,
|
||||
phase: None,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -575,6 +583,7 @@ impl TurnItem {
|
||||
TurnItem::Reasoning(item) => item.id.clone(),
|
||||
TurnItem::WebSearch(item) => item.id.clone(),
|
||||
TurnItem::ImageView(item) => item.id.clone(),
|
||||
TurnItem::Sleep(item) => item.id.clone(),
|
||||
TurnItem::ImageGeneration(item) => item.id.clone(),
|
||||
TurnItem::FileChange(item) => item.id.clone(),
|
||||
TurnItem::McpToolCall(item) => item.id.clone(),
|
||||
@@ -595,6 +604,7 @@ impl TurnItem {
|
||||
path: item.path.clone(),
|
||||
})]
|
||||
}
|
||||
TurnItem::Sleep(_) => Vec::new(),
|
||||
TurnItem::ImageGeneration(item) => vec![item.as_legacy_event()],
|
||||
TurnItem::FileChange(item) => item
|
||||
.as_legacy_end_event(String::new())
|
||||
|
||||
@@ -9,6 +9,7 @@ pub use session_id::SessionId;
|
||||
pub use thread_id::ThreadId;
|
||||
pub use tool_name::ToolName;
|
||||
pub mod approvals;
|
||||
pub mod capabilities;
|
||||
pub mod config_types;
|
||||
pub mod dynamic_tools;
|
||||
pub mod error;
|
||||
|
||||
+524
-88
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,22 @@
|
||||
//! These types are serialized across core, TUI, app-server, and SDK boundaries, so field defaults
|
||||
//! are used to preserve compatibility when older payloads omit newly introduced attributes.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::schema::InstanceType;
|
||||
use schemars::schema::Metadata;
|
||||
use schemars::schema::Schema;
|
||||
use schemars::schema::SchemaObject;
|
||||
use schemars::schema::StringValidation;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use serde::de::DeserializeOwned;
|
||||
use strum::IntoEnumIterator;
|
||||
use serde::de::Error;
|
||||
use strum_macros::Display;
|
||||
use strum_macros::EnumIter;
|
||||
use tracing::warn;
|
||||
@@ -22,28 +29,14 @@ use crate::config_types::ReasoningSummary;
|
||||
use crate::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use crate::config_types::ServiceTier;
|
||||
use crate::config_types::Verbosity;
|
||||
use crate::protocol::MultiAgentVersion;
|
||||
|
||||
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
|
||||
pub const SPEED_TIER_FAST: &str = "fast";
|
||||
|
||||
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
|
||||
#[derive(
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Display,
|
||||
JsonSchema,
|
||||
TS,
|
||||
EnumIter,
|
||||
Hash,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, TS, Hash)]
|
||||
#[ts(type = "string")]
|
||||
pub enum ReasoningEffort {
|
||||
None,
|
||||
Minimal,
|
||||
@@ -52,14 +45,87 @@ pub enum ReasoningEffort {
|
||||
Medium,
|
||||
High,
|
||||
XHigh,
|
||||
/// A model-defined effort value that this client does not know yet.
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl ReasoningEffort {
|
||||
/// Returns the exact value used on the wire.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::Minimal => "minimal",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
Self::Custom(effort) => effort,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ReasoningEffort {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for ReasoningEffort {
|
||||
fn schema_name() -> String {
|
||||
"ReasoningEffort".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(
|
||||
"A non-empty reasoning effort value advertised by the model.".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
})),
|
||||
string: Some(Box::new(StringValidation {
|
||||
min_length: Some(1),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ReasoningEffort {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ReasoningEffort {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let effort = String::deserialize(deserializer)?;
|
||||
effort.parse().map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ReasoningEffort {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_value(serde_json::Value::String(s.to_string()))
|
||||
.map_err(|_| format!("invalid reasoning_effort: {s}"))
|
||||
match s {
|
||||
"none" => Ok(Self::None),
|
||||
"minimal" => Ok(Self::Minimal),
|
||||
"low" => Ok(Self::Low),
|
||||
"medium" => Ok(Self::Medium),
|
||||
"high" => Ok(Self::High),
|
||||
"xhigh" => Ok(Self::XHigh),
|
||||
"" => Err("reasoning_effort must not be empty".to_string()),
|
||||
effort => Ok(Self::Custom(effort.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +173,6 @@ pub struct ReasoningEffortPreset {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
|
||||
pub struct ModelUpgrade {
|
||||
pub id: String,
|
||||
pub reasoning_effort_mapping: Option<HashMap<ReasoningEffort, ReasoningEffort>>,
|
||||
pub migration_config_key: String,
|
||||
pub model_link: Option<String>,
|
||||
pub upgrade_copy: Option<String>,
|
||||
@@ -324,6 +389,9 @@ pub struct ModelInfo {
|
||||
/// context window when available.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_compact_token_limit: Option<i64>,
|
||||
/// Opaque identifier for compaction-compatible model configurations.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub comp_hash: Option<String>,
|
||||
/// Percentage of the context window considered usable for inputs, after
|
||||
/// reserving headroom for system prompts, tool overhead, and model output.
|
||||
#[serde(default = "default_effective_context_window_percent")]
|
||||
@@ -339,6 +407,8 @@ pub struct ModelInfo {
|
||||
pub used_fallback_model_metadata: bool,
|
||||
#[serde(default)]
|
||||
pub supports_search_tool: bool,
|
||||
#[serde(default)]
|
||||
pub use_responses_lite: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_review_model_override: Option<String>,
|
||||
#[serde(
|
||||
@@ -347,6 +417,12 @@ pub struct ModelInfo {
|
||||
deserialize_with = "deserialize_optional_model_selector"
|
||||
)]
|
||||
pub tool_mode: Option<ToolMode>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_optional_model_selector"
|
||||
)]
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
}
|
||||
|
||||
impl ModelInfo {
|
||||
@@ -494,9 +570,6 @@ impl From<ModelInfo> for ModelPreset {
|
||||
is_default: false, // default is the highest priority available model
|
||||
upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade {
|
||||
id: upgrade.model.clone(),
|
||||
reasoning_effort_mapping: reasoning_effort_mapping_from_presets(
|
||||
&info.supported_reasoning_levels,
|
||||
),
|
||||
migration_config_key: info.slug.clone(),
|
||||
// todo(aibrahim): add the model link here.
|
||||
model_link: None,
|
||||
@@ -564,47 +637,12 @@ impl ModelPreset {
|
||||
}
|
||||
}
|
||||
|
||||
fn reasoning_effort_mapping_from_presets(
|
||||
presets: &[ReasoningEffortPreset],
|
||||
) -> Option<HashMap<ReasoningEffort, ReasoningEffort>> {
|
||||
if presets.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Map every canonical effort to the closest supported effort for the new model.
|
||||
let supported: Vec<ReasoningEffort> = presets.iter().map(|p| p.effort).collect();
|
||||
let mut map = HashMap::new();
|
||||
for effort in ReasoningEffort::iter() {
|
||||
let nearest = nearest_effort(effort, &supported);
|
||||
map.insert(effort, nearest);
|
||||
}
|
||||
Some(map)
|
||||
}
|
||||
|
||||
fn effort_rank(effort: ReasoningEffort) -> i32 {
|
||||
match effort {
|
||||
ReasoningEffort::None => 0,
|
||||
ReasoningEffort::Minimal => 1,
|
||||
ReasoningEffort::Low => 2,
|
||||
ReasoningEffort::Medium => 3,
|
||||
ReasoningEffort::High => 4,
|
||||
ReasoningEffort::XHigh => 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_effort(target: ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
|
||||
let target_rank = effort_rank(target);
|
||||
supported
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|candidate| (effort_rank(*candidate) - target_rank).abs())
|
||||
.unwrap_or(target)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::from_str;
|
||||
use serde_json::to_string;
|
||||
|
||||
fn test_model(spec: Option<ModelMessages>) -> ModelInfo {
|
||||
ModelInfo {
|
||||
@@ -636,13 +674,16 @@ mod tests {
|
||||
context_window: None,
|
||||
max_context_window: None,
|
||||
auto_compact_token_limit: None,
|
||||
comp_hash: None,
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: vec![],
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
use_responses_lite: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,16 +696,58 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_from_str_accepts_known_values() {
|
||||
assert_eq!("high".parse(), Ok(ReasoningEffort::High));
|
||||
assert_eq!("minimal".parse(), Ok(ReasoningEffort::Minimal));
|
||||
fn reasoning_effort_accepts_known_and_custom_values() {
|
||||
let custom = ReasoningEffort::Custom("max".to_string());
|
||||
let deserialized = from_str::<ReasoningEffort>(r#""max""#)
|
||||
.expect("custom reasoning effort should deserialize");
|
||||
let serialized = to_string(&custom).expect("custom reasoning effort should serialize");
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
"high".parse(),
|
||||
"max".parse(),
|
||||
deserialized,
|
||||
serialized,
|
||||
custom.to_string(),
|
||||
),
|
||||
(
|
||||
Ok(ReasoningEffort::High),
|
||||
Ok(custom.clone()),
|
||||
custom,
|
||||
r#""max""#.to_string(),
|
||||
"max".to_string(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_from_str_rejects_unknown_values() {
|
||||
fn reasoning_effort_rejects_empty_values() {
|
||||
assert_eq!(
|
||||
"unsupported".parse::<ReasoningEffort>(),
|
||||
Err("invalid reasoning_effort: unsupported".to_string())
|
||||
"".parse::<ReasoningEffort>(),
|
||||
Err("reasoning_effort must not be empty".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_json_schema_is_an_open_string() {
|
||||
let mut effort_generator = SchemaGenerator::default();
|
||||
|
||||
assert_eq!(
|
||||
ReasoningEffort::json_schema(&mut effort_generator),
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(
|
||||
"A non-empty reasoning effort value advertised by the model.".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
})),
|
||||
string: Some(Box::new(StringValidation {
|
||||
min_length: Some(1),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -860,6 +943,8 @@ mod tests {
|
||||
assert!(!model.supports_image_detail_original);
|
||||
assert_eq!(model.web_search_tool_type, WebSearchToolType::Text);
|
||||
assert!(!model.supports_search_tool);
|
||||
assert!(!model.use_responses_lite);
|
||||
assert_eq!(model.comp_hash, None);
|
||||
assert_eq!(model.auto_review_model_override, None);
|
||||
assert_eq!(model.tool_mode, None);
|
||||
}
|
||||
@@ -901,6 +986,22 @@ mod tests {
|
||||
assert!(!object.contains_key("tool_mode"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_info_treats_unknown_multi_agent_version_as_omitted() {
|
||||
let mut value =
|
||||
serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");
|
||||
let object = value
|
||||
.as_object_mut()
|
||||
.expect("model info should be an object");
|
||||
object.insert(
|
||||
"multi_agent_version".to_string(),
|
||||
serde_json::Value::String("future_multi_agent_version".to_string()),
|
||||
);
|
||||
let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");
|
||||
|
||||
assert_eq!(model.multi_agent_version, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_context_window_prefers_context_window() {
|
||||
let model = ModelInfo {
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use globset::GlobBuilder;
|
||||
use globset::GlobMatcher;
|
||||
use schemars::JsonSchema;
|
||||
@@ -176,11 +177,31 @@ impl FileSystemSpecialPath {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct FileSystemSandboxEntry {
|
||||
pub path: FileSystemPath,
|
||||
pub struct FileSystemSandboxEntry<PathType = AbsolutePathBuf> {
|
||||
pub path: FileSystemPath<PathType>,
|
||||
pub access: FileSystemAccessMode,
|
||||
}
|
||||
|
||||
impl From<FileSystemSandboxEntry<AbsolutePathBuf>> for FileSystemSandboxEntry<PathUri> {
|
||||
fn from(value: FileSystemSandboxEntry<AbsolutePathBuf>) -> Self {
|
||||
FileSystemSandboxEntry {
|
||||
path: value.path.into(),
|
||||
access: value.access,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FileSystemSandboxEntry<PathUri>> for FileSystemSandboxEntry<AbsolutePathBuf> {
|
||||
type Error = io::Error;
|
||||
|
||||
fn try_from(value: FileSystemSandboxEntry<PathUri>) -> Result<Self, Self::Error> {
|
||||
Ok(FileSystemSandboxEntry {
|
||||
path: value.path.try_into()?,
|
||||
access: value.access,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
|
||||
)]
|
||||
@@ -338,9 +359,9 @@ enum InvalidDenyReadGlobBehavior {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum FileSystemPath {
|
||||
pub enum FileSystemPath<PathType = AbsolutePathBuf> {
|
||||
Path {
|
||||
path: AbsolutePathBuf,
|
||||
path: PathType,
|
||||
},
|
||||
/// A git-style glob pattern. Pattern entries currently support
|
||||
/// FileSystemAccessMode::Deny only.
|
||||
@@ -352,28 +373,58 @@ pub enum FileSystemPath {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<FileSystemPath<AbsolutePathBuf>> for FileSystemPath<PathUri> {
|
||||
fn from(value: FileSystemPath<AbsolutePathBuf>) -> Self {
|
||||
match value {
|
||||
FileSystemPath::Path { path } => FileSystemPath::Path {
|
||||
path: PathUri::from_abs_path(&path),
|
||||
},
|
||||
FileSystemPath::GlobPattern { pattern } => FileSystemPath::GlobPattern { pattern },
|
||||
FileSystemPath::Special { value } => FileSystemPath::Special { value },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FileSystemPath<PathUri>> for FileSystemPath<AbsolutePathBuf> {
|
||||
type Error = io::Error;
|
||||
|
||||
fn try_from(value: FileSystemPath<PathUri>) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
FileSystemPath::Path { path } => FileSystemPath::Path {
|
||||
path: path.to_abs_path()?,
|
||||
},
|
||||
FileSystemPath::GlobPattern { pattern } => FileSystemPath::GlobPattern { pattern },
|
||||
FileSystemPath::Special { value } => FileSystemPath::Special { value },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const PROJECT_ROOTS_GLOB_PATTERN_PREFIX: &str = "codex-project-roots://";
|
||||
|
||||
pub fn project_roots_glob_pattern(subpath: &Path) -> String {
|
||||
format!("{PROJECT_ROOTS_GLOB_PATTERN_PREFIX}{}", subpath.display())
|
||||
}
|
||||
|
||||
fn read_only_file_system_entries() -> Vec<FileSystemSandboxEntry> {
|
||||
vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}]
|
||||
}
|
||||
|
||||
impl Default for FileSystemSandboxPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kind: FileSystemSandboxKind::Restricted,
|
||||
glob_scan_max_depth: None,
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}],
|
||||
}
|
||||
Self::read_only()
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystemSandboxPolicy {
|
||||
pub fn read_only() -> Self {
|
||||
Self::restricted(read_only_file_system_entries())
|
||||
}
|
||||
|
||||
pub fn unrestricted() -> Self {
|
||||
Self {
|
||||
kind: FileSystemSandboxKind::Unrestricted,
|
||||
|
||||
+408
-328
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,14 @@ impl From<AdditionalPermissionProfile> for RequestPermissionProfile {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct RequestPermissionsArgs {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "environment_id",
|
||||
alias = "environmentId",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional)]
|
||||
pub environment_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
pub permissions: RequestPermissionProfile,
|
||||
@@ -71,6 +79,15 @@ pub struct RequestPermissionsEvent {
|
||||
/// Uses `#[serde(default)]` for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub turn_id: String,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "environmentId",
|
||||
alias = "environment_id",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional)]
|
||||
#[ts(rename = "environmentId")]
|
||||
pub environment_id: Option<String>,
|
||||
#[ts(type = "number")]
|
||||
pub started_at_ms: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -31,6 +31,9 @@ pub struct RequestUserInputQuestion {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct RequestUserInputArgs {
|
||||
pub questions: Vec<RequestUserInputQuestion>,
|
||||
#[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(rename = "autoResolutionMs")]
|
||||
pub auto_resolution_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -52,4 +55,7 @@ pub struct RequestUserInputEvent {
|
||||
#[serde(default)]
|
||||
pub turn_id: String,
|
||||
pub questions: Vec<RequestUserInputQuestion>,
|
||||
#[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(rename = "autoResolutionMs")]
|
||||
pub auto_resolution_ms: Option<u64>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user