Skip to content

Commit 85cd533

Browse files
committed
feat: annotate conversations with model_provider for filtering
1 parent a4be4d7 commit 85cd533

File tree

14 files changed

+469
-57
lines changed

14 files changed

+469
-57
lines changed

codex-rs/app-server-protocol/src/protocol.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ pub struct ListConversationsParams {
313313
/// Opaque pagination cursor returned by a previous call.
314314
#[serde(skip_serializing_if = "Option::is_none")]
315315
pub cursor: Option<String>,
316+
/// Optional model provider filter (matches against session metadata).
317+
#[serde(skip_serializing_if = "Option::is_none")]
318+
pub model_provider: Option<Vec<String>>,
316319
}
317320

318321
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -324,6 +327,9 @@ pub struct ConversationSummary {
324327
/// RFC3339 timestamp string for the session start, if available.
325328
#[serde(skip_serializing_if = "Option::is_none")]
326329
pub timestamp: Option<String>,
330+
/// Optional model provider recorded for the session.
331+
#[serde(skip_serializing_if = "Option::is_none")]
332+
pub model_provider: Option<String>,
327333
}
328334

329335
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -818,19 +818,36 @@ impl CodexMessageProcessor {
818818
request_id: RequestId,
819819
params: ListConversationsParams,
820820
) {
821-
let page_size = params.page_size.unwrap_or(25);
821+
let ListConversationsParams {
822+
page_size,
823+
cursor,
824+
model_provider,
825+
} = params;
826+
let page_size = page_size.unwrap_or(25);
822827
// Decode the optional cursor string to a Cursor via serde (Cursor implements Deserialize from string)
823-
let cursor_obj: Option<RolloutCursor> = match params.cursor {
828+
let cursor_obj: Option<RolloutCursor> = match cursor {
824829
Some(s) => serde_json::from_str::<RolloutCursor>(&format!("\"{s}\"")).ok(),
825830
None => None,
826831
};
827832
let cursor_ref = cursor_obj.as_ref();
833+
let model_provider_filter = match model_provider {
834+
Some(providers) => {
835+
if providers.is_empty() {
836+
None
837+
} else {
838+
Some(providers)
839+
}
840+
}
841+
None => Some(vec![self.config.model_provider_id.clone()]),
842+
};
843+
let model_provider_slice = model_provider_filter.as_deref();
828844

829845
let page = match RolloutRecorder::list_conversations(
830846
&self.config.codex_home,
831847
page_size,
832848
cursor_ref,
833849
INTERACTIVE_SESSION_SOURCES,
850+
model_provider_slice,
834851
)
835852
.await
836853
{
@@ -1640,12 +1657,15 @@ fn extract_conversation_summary(
16401657
} else {
16411658
Some(session_meta.timestamp.clone())
16421659
};
1660+
let conversation_id = session_meta.id;
1661+
let model_provider = session_meta.model_provider;
16431662

16441663
Some(ConversationSummary {
1645-
conversation_id: session_meta.id,
1664+
conversation_id,
16461665
timestamp,
16471666
path,
16481667
preview: preview.to_string(),
1668+
model_provider,
16491669
})
16501670
}
16511671

@@ -1669,7 +1689,8 @@ mod tests {
16691689
"cwd": "/",
16701690
"originator": "codex",
16711691
"cli_version": "0.0.0",
1672-
"instructions": null
1692+
"instructions": null,
1693+
"model_provider": "test-provider"
16731694
}),
16741695
json!({
16751696
"type": "message",
@@ -1698,6 +1719,7 @@ mod tests {
16981719
);
16991720
assert_eq!(summary.path, path);
17001721
assert_eq!(summary.preview, "Count to 5");
1722+
assert_eq!(summary.model_provider, Some("test-provider".to_string()));
17011723
Ok(())
17021724
}
17031725
}

codex-rs/app-server/tests/suite/list_resume.rs

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,21 @@ async fn test_list_and_resume_conversations() {
3030
"2025-01-02T12-00-00",
3131
"2025-01-02T12:00:00Z",
3232
"Hello A",
33+
Some("openai"),
3334
);
3435
create_fake_rollout(
3536
codex_home.path(),
3637
"2025-01-01T13-00-00",
3738
"2025-01-01T13:00:00Z",
3839
"Hello B",
40+
Some("openai"),
3941
);
4042
create_fake_rollout(
4143
codex_home.path(),
4244
"2025-01-01T12-00-00",
4345
"2025-01-01T12:00:00Z",
4446
"Hello C",
47+
None,
4548
);
4649

4750
let mut mcp = McpProcess::new(codex_home.path())
@@ -57,6 +60,7 @@ async fn test_list_and_resume_conversations() {
5760
.send_list_conversations_request(ListConversationsParams {
5861
page_size: Some(2),
5962
cursor: None,
63+
model_provider: None,
6064
})
6165
.await
6266
.expect("send listConversations");
@@ -74,6 +78,8 @@ async fn test_list_and_resume_conversations() {
7478
// Newest first; preview text should match
7579
assert_eq!(items[0].preview, "Hello A");
7680
assert_eq!(items[1].preview, "Hello B");
81+
assert_eq!(items[0].model_provider.as_deref(), Some("openai"));
82+
assert_eq!(items[1].model_provider.as_deref(), Some("openai"));
7783
assert!(items[0].path.is_absolute());
7884
assert!(next_cursor.is_some());
7985

@@ -82,6 +88,7 @@ async fn test_list_and_resume_conversations() {
8288
.send_list_conversations_request(ListConversationsParams {
8389
page_size: Some(2),
8490
cursor: next_cursor,
91+
model_provider: None,
8592
})
8693
.await
8794
.expect("send listConversations page 2");
@@ -99,7 +106,91 @@ async fn test_list_and_resume_conversations() {
99106
} = to_response::<ListConversationsResponse>(resp2).expect("deserialize response");
100107
assert_eq!(items2.len(), 1);
101108
assert_eq!(items2[0].preview, "Hello C");
102-
assert!(next2.is_some());
109+
assert_eq!(items2[0].model_provider, None);
110+
assert_eq!(next2, None);
111+
112+
// Add a conversation with an explicit non-OpenAI provider for filter tests.
113+
create_fake_rollout(
114+
codex_home.path(),
115+
"2025-01-01T11-30-00",
116+
"2025-01-01T11:30:00Z",
117+
"Hello TP",
118+
Some("test-provider"),
119+
);
120+
121+
// Filtering by model provider should return only matching sessions.
122+
let filter_req_id = mcp
123+
.send_list_conversations_request(ListConversationsParams {
124+
page_size: Some(10),
125+
cursor: None,
126+
model_provider: Some(vec!["test-provider".to_string()]),
127+
})
128+
.await
129+
.expect("send listConversations filtered");
130+
let filter_resp: JSONRPCResponse = timeout(
131+
DEFAULT_READ_TIMEOUT,
132+
mcp.read_stream_until_response_message(RequestId::Integer(filter_req_id)),
133+
)
134+
.await
135+
.expect("listConversations filtered timeout")
136+
.expect("listConversations filtered resp");
137+
let ListConversationsResponse {
138+
items: filtered_items,
139+
next_cursor: filtered_next,
140+
} = to_response::<ListConversationsResponse>(filter_resp).expect("deserialize filtered");
141+
assert_eq!(filtered_items.len(), 1);
142+
assert_eq!(filtered_next, None);
143+
assert_eq!(filtered_items[0].preview, "Hello TP");
144+
assert_eq!(
145+
filtered_items[0].model_provider.as_deref(),
146+
Some("test-provider")
147+
);
148+
149+
// Empty filter should include every session regardless of provider metadata.
150+
let unfiltered_req_id = mcp
151+
.send_list_conversations_request(ListConversationsParams {
152+
page_size: Some(10),
153+
cursor: None,
154+
model_provider: Some(Vec::new()),
155+
})
156+
.await
157+
.expect("send listConversations unfiltered");
158+
let unfiltered_resp: JSONRPCResponse = timeout(
159+
DEFAULT_READ_TIMEOUT,
160+
mcp.read_stream_until_response_message(RequestId::Integer(unfiltered_req_id)),
161+
)
162+
.await
163+
.expect("listConversations unfiltered timeout")
164+
.expect("listConversations unfiltered resp");
165+
let ListConversationsResponse {
166+
items: unfiltered_items,
167+
next_cursor: unfiltered_next,
168+
} = to_response::<ListConversationsResponse>(unfiltered_resp)
169+
.expect("deserialize unfiltered response");
170+
assert_eq!(unfiltered_items.len(), 4);
171+
assert!(unfiltered_next.is_none());
172+
173+
let empty_req_id = mcp
174+
.send_list_conversations_request(ListConversationsParams {
175+
page_size: Some(10),
176+
cursor: None,
177+
model_provider: Some(vec!["other".to_string()]),
178+
})
179+
.await
180+
.expect("send listConversations filtered empty");
181+
let empty_resp: JSONRPCResponse = timeout(
182+
DEFAULT_READ_TIMEOUT,
183+
mcp.read_stream_until_response_message(RequestId::Integer(empty_req_id)),
184+
)
185+
.await
186+
.expect("listConversations filtered empty timeout")
187+
.expect("listConversations filtered empty resp");
188+
let ListConversationsResponse {
189+
items: empty_items,
190+
next_cursor: empty_next,
191+
} = to_response::<ListConversationsResponse>(empty_resp).expect("deserialize filtered empty");
192+
assert!(empty_items.is_empty());
193+
assert!(empty_next.is_none());
103194

104195
// Now resume one of the sessions and expect a SessionConfigured notification and response.
105196
let resume_req_id = mcp
@@ -152,7 +243,13 @@ async fn test_list_and_resume_conversations() {
152243
assert!(!conversation_id.to_string().is_empty());
153244
}
154245

155-
fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str) {
246+
fn create_fake_rollout(
247+
codex_home: &Path,
248+
filename_ts: &str,
249+
meta_rfc3339: &str,
250+
preview: &str,
251+
model_provider: Option<&str>,
252+
) {
156253
let uuid = Uuid::new_v4();
157254
// sessions/YYYY/MM/DD/ derived from filename_ts (YYYY-MM-DDThh-mm-ss)
158255
let year = &filename_ts[0..4];
@@ -164,18 +261,22 @@ fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str,
164261
let file_path = dir.join(format!("rollout-{filename_ts}-{uuid}.jsonl"));
165262
let mut lines = Vec::new();
166263
// Meta line with timestamp (flattened meta in payload for new schema)
264+
let mut payload = json!({
265+
"id": uuid,
266+
"timestamp": meta_rfc3339,
267+
"cwd": "/",
268+
"originator": "codex",
269+
"cli_version": "0.0.0",
270+
"instructions": null,
271+
});
272+
if let Some(provider) = model_provider {
273+
payload["model_provider"] = json!(provider);
274+
}
167275
lines.push(
168276
json!({
169277
"timestamp": meta_rfc3339,
170278
"type": "session_meta",
171-
"payload": {
172-
"id": uuid,
173-
"timestamp": meta_rfc3339,
174-
"cwd": "/",
175-
"originator": "codex",
176-
"cli_version": "0.0.0",
177-
"instructions": null
178-
}
279+
"payload": payload
179280
})
180281
.to_string(),
181282
);

0 commit comments

Comments
 (0)