Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codex-rs/core/src/realtime_context_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ fn thread_metadata(cwd: &str, title: &str, first_user_message: &str) -> ThreadMe
agent_nickname: None,
agent_role: None,
model_provider: "test-provider".to_string(),
model: Some("gpt-5".to_string()),
reasoning_effort: None,
cwd: PathBuf::from(cwd),
cli_version: "test".to_string(),
title: title.to_string(),
Expand Down
24 changes: 24 additions & 0 deletions codex-rs/protocol/src/openai_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! are used to preserve compatibility when older payloads omit newly introduced attributes.

use std::collections::HashMap;
use std::str::FromStr;

use schemars::JsonSchema;
use serde::Deserialize;
Expand Down Expand Up @@ -48,6 +49,15 @@ pub enum ReasoningEffort {
XHigh,
}

impl FromStr for ReasoningEffort {
type Err = String;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic string type error - open to have a custom error type if necessary but feel like an overkill.


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}"))
}
}
Comment on lines +52 to +59
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simple string to enum parser so that read from db can be correctly converted. Better to keep such parser on the enum def side so that we can reuse down the line.


/// Canonical user-input modality tags advertised by a model.
#[derive(
Debug,
Expand Down Expand Up @@ -552,6 +562,20 @@ 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));
}

#[test]
fn reasoning_effort_from_str_rejects_unknown_values() {
assert_eq!(
"unsupported".parse::<ReasoningEffort>(),
Err("invalid reasoning_effort: unsupported".to_string())
);
}

#[test]
fn get_model_instructions_uses_template_when_placeholder_present() {
let model = test_model(Some(ModelMessages {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE threads ADD COLUMN model TEXT;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@charley-oai if there is a migration here, maybe good to package yours in the same release

ALTER TABLE threads ADD COLUMN reasoning_effort TEXT;
Comment on lines +1 to +2
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capture the latest model and reasoning effort per thread.

72 changes: 71 additions & 1 deletion codex-rs/state/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem)
if metadata.cwd.as_os_str().is_empty() {
metadata.cwd = turn_ctx.cwd.clone();
}
metadata.model = Some(turn_ctx.model.clone());
metadata.reasoning_effort = turn_ctx.effort;
metadata.sandbox_policy = enum_to_string(&turn_ctx.sandbox_policy);
metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy);
}
Expand Down Expand Up @@ -141,6 +143,7 @@ mod tests {
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::RolloutItem;
Expand Down Expand Up @@ -312,7 +315,7 @@ mod tests {
personality: None,
collaboration_mode: None,
realtime_active: None,
effort: None,
effort: Some(ReasoningEffort::High),
summary: ReasoningSummary::Auto,
user_instructions: None,
developer_instructions: None,
Expand All @@ -325,6 +328,71 @@ mod tests {
assert_eq!(metadata.cwd, PathBuf::from("/fallback/workspace"));
}

#[test]
fn turn_context_sets_model_and_reasoning_effort() {
let mut metadata = metadata_for_test();

apply_rollout_item(
&mut metadata,
&RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()),
trace_id: None,
cwd: PathBuf::from("/fallback/workspace"),
current_date: None,
timezone: None,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
network: None,
model: "gpt-5".to_string(),
personality: None,
collaboration_mode: None,
realtime_active: None,
effort: Some(ReasoningEffort::High),
summary: ReasoningSummary::Auto,
user_instructions: None,
developer_instructions: None,
final_output_json_schema: None,
truncation_policy: None,
}),
"test-provider",
);

assert_eq!(metadata.model.as_deref(), Some("gpt-5"));
assert_eq!(metadata.reasoning_effort, Some(ReasoningEffort::High));
}

#[test]
fn session_meta_does_not_set_model_or_reasoning_effort() {
let mut metadata = metadata_for_test();
let thread_id = metadata.id;

apply_rollout_item(
&mut metadata,
&RolloutItem::SessionMeta(SessionMetaLine {
meta: SessionMeta {
id: thread_id,
forked_from_id: None,
timestamp: "2026-02-26T00:00:00.000Z".to_string(),
cwd: PathBuf::from("/workspace"),
originator: "codex_cli_rs".to_string(),
cli_version: "0.0.0".to_string(),
source: SessionSource::Cli,
agent_nickname: None,
agent_role: None,
model_provider: Some("openai".to_string()),
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
},
git: None,
}),
"test-provider",
);

assert_eq!(metadata.model, None);
assert_eq!(metadata.reasoning_effort, None);
}

fn metadata_for_test() -> ThreadMetadata {
let id = ThreadId::from_string(&Uuid::from_u128(42).to_string()).expect("thread id");
let created_at = DateTime::<Utc>::from_timestamp(1_735_689_600, 0).expect("timestamp");
Expand All @@ -337,6 +405,8 @@ mod tests {
agent_nickname: None,
agent_role: None,
model_provider: "openai".to_string(),
model: None,
reasoning_effort: None,
cwd: PathBuf::from("/tmp"),
cli_version: "0.0.0".to_string(),
title: String::new(),
Expand Down
106 changes: 106 additions & 0 deletions codex-rs/state/src/model/thread_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use chrono::DateTime;
use chrono::Timelike;
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
Expand Down Expand Up @@ -70,6 +71,10 @@ pub struct ThreadMetadata {
pub agent_role: Option<String>,
/// The model provider identifier.
pub model_provider: String,
/// The latest observed model for the thread.
pub model: Option<String>,
/// The latest observed reasoning effort for the thread.
pub reasoning_effort: Option<ReasoningEffort>,
/// The working directory for the thread.
pub cwd: PathBuf,
/// Version of the CLI that created the thread.
Expand Down Expand Up @@ -181,6 +186,8 @@ impl ThreadMetadataBuilder {
.model_provider
.clone()
.unwrap_or_else(|| default_provider.to_string()),
model: None,
reasoning_effort: None,
cwd: self.cwd.clone(),
cli_version: self.cli_version.clone().unwrap_or_default(),
title: String::new(),
Expand Down Expand Up @@ -237,6 +244,12 @@ impl ThreadMetadata {
if self.model_provider != other.model_provider {
diffs.push("model_provider");
}
if self.model != other.model {
diffs.push("model");
}
if self.reasoning_effort != other.reasoning_effort {
diffs.push("reasoning_effort");
}
if self.cwd != other.cwd {
diffs.push("cwd");
}
Expand Down Expand Up @@ -288,6 +301,8 @@ pub(crate) struct ThreadRow {
agent_nickname: Option<String>,
agent_role: Option<String>,
model_provider: String,
model: Option<String>,
reasoning_effort: Option<String>,
cwd: String,
cli_version: String,
title: String,
Expand All @@ -312,6 +327,8 @@ impl ThreadRow {
agent_nickname: row.try_get("agent_nickname")?,
agent_role: row.try_get("agent_role")?,
model_provider: row.try_get("model_provider")?,
model: row.try_get("model")?,
reasoning_effort: row.try_get("reasoning_effort")?,
cwd: row.try_get("cwd")?,
cli_version: row.try_get("cli_version")?,
title: row.try_get("title")?,
Expand Down Expand Up @@ -340,6 +357,8 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
agent_nickname,
agent_role,
model_provider,
model,
reasoning_effort,
cwd,
cli_version,
title,
Expand All @@ -361,6 +380,9 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
agent_nickname,
agent_role,
model_provider,
model,
reasoning_effort: reasoning_effort
.and_then(|value| value.parse::<ReasoningEffort>().ok()),
cwd: PathBuf::from(cwd),
cli_version,
title,
Expand Down Expand Up @@ -404,3 +426,87 @@ pub struct BackfillStats {
/// The number of rows that failed to upsert.
pub failed: usize,
}

#[cfg(test)]
mod tests {
use super::ThreadMetadata;
use super::ThreadRow;
use chrono::DateTime;
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_protocol::openai_models::ReasoningEffort;
use pretty_assertions::assert_eq;
use std::path::PathBuf;

fn thread_row(reasoning_effort: Option<&str>) -> ThreadRow {
ThreadRow {
id: "00000000-0000-0000-0000-000000000123".to_string(),
rollout_path: "/tmp/rollout-123.jsonl".to_string(),
created_at: 1_700_000_000,
updated_at: 1_700_000_100,
source: "cli".to_string(),
agent_nickname: None,
agent_role: None,
model_provider: "openai".to_string(),
model: Some("gpt-5".to_string()),
reasoning_effort: reasoning_effort.map(str::to_string),
cwd: "/tmp/workspace".to_string(),
cli_version: "0.0.0".to_string(),
title: String::new(),
sandbox_policy: "read-only".to_string(),
approval_mode: "on-request".to_string(),
tokens_used: 1,
first_user_message: String::new(),
archived_at: None,
git_sha: None,
git_branch: None,
git_origin_url: None,
}
}

fn expected_thread_metadata(reasoning_effort: Option<ReasoningEffort>) -> ThreadMetadata {
ThreadMetadata {
id: ThreadId::from_string("00000000-0000-0000-0000-000000000123")
.expect("valid thread id"),
rollout_path: PathBuf::from("/tmp/rollout-123.jsonl"),
created_at: DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("timestamp"),
updated_at: DateTime::<Utc>::from_timestamp(1_700_000_100, 0).expect("timestamp"),
source: "cli".to_string(),
agent_nickname: None,
agent_role: None,
model_provider: "openai".to_string(),
model: Some("gpt-5".to_string()),
reasoning_effort,
cwd: PathBuf::from("/tmp/workspace"),
cli_version: "0.0.0".to_string(),
title: String::new(),
sandbox_policy: "read-only".to_string(),
approval_mode: "on-request".to_string(),
tokens_used: 1,
first_user_message: None,
archived_at: None,
git_sha: None,
git_branch: None,
git_origin_url: None,
}
}

#[test]
fn thread_row_parses_reasoning_effort() {
let metadata = ThreadMetadata::try_from(thread_row(Some("high")))
.expect("thread metadata should parse");

assert_eq!(
metadata,
expected_thread_metadata(Some(ReasoningEffort::High))
);
}

#[test]
fn thread_row_ignores_unknown_reasoning_effort_values() {
let metadata = ThreadMetadata::try_from(thread_row(Some("future")))
.expect("thread metadata should parse");

assert_eq!(metadata, expected_thread_metadata(None));
}
}
2 changes: 2 additions & 0 deletions codex-rs/state/src/runtime/memories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ SELECT
agent_nickname,
agent_role,
model_provider,
model,
reasoning_effort,
cwd,
cli_version,
title,
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/state/src/runtime/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use chrono::Utc;
#[cfg(test)]
use codex_protocol::ThreadId;
#[cfg(test)]
use codex_protocol::openai_models::ReasoningEffort;
#[cfg(test)]
use codex_protocol::protocol::AskForApproval;
#[cfg(test)]
use codex_protocol::protocol::SandboxPolicy;
Expand Down Expand Up @@ -49,6 +51,8 @@ pub(super) fn test_thread_metadata(
agent_nickname: None,
agent_role: None,
model_provider: "test-provider".to_string(),
model: Some("gpt-5".to_string()),
reasoning_effort: Some(ReasoningEffort::Medium),
cwd,
cli_version: "0.0.0".to_string(),
title: String::new(),
Expand Down
Loading
Loading