Skip to content

Commit fc39dbd

Browse files
clkaoclaude
andauthored
Show which model Codex sessions use (#189)
## Summary - Codex sessions now display the model name (e.g. gpt-5-codex, o3-pro) alongside each message, matching what Gemini sessions already show - Makes it easy to see at a glance which model was used for each turn, including when models change mid-session ## How it works Codex writes a `turn_context` entry at the start of each user turn containing the active model. The parser now reads this and stamps it on all messages in that turn. ## Changes - Parse `turn_context` entries from Codex JSONL and extract the `model` field - Track current model in the session builder and apply it to all subsequent messages (both text and tool calls) - Add `CodexTurnContextJSON` test helper - Three test cases: model applied to messages, model changes across turns, no turn_context ## Test plan - [x] New unit tests pass (`TestParseCodexSession_TurnContextModel`) - [x] All existing Codex parser tests pass (27 tests) - [x] `go vet` and `go fmt` clean - [ ] Verify with real Codex session files containing `turn_context` entries 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e44904c commit fc39dbd

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

internal/parser/codex.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
const (
1616
codexTypeSessionMeta = "session_meta"
1717
codexTypeResponseItem = "response_item"
18+
codexTypeTurnContext = "turn_context"
1819
codexOriginatorExec = "codex_exec"
1920
)
2021

@@ -29,6 +30,7 @@ type codexSessionBuilder struct {
2930
project string
3031
ordinal int
3132
includeExec bool
33+
currentModel string
3234
}
3335

3436
func newCodexSessionBuilder(
@@ -64,6 +66,8 @@ func (b *codexSessionBuilder) processLine(
6466
switch gjson.Get(line, "type").Str {
6567
case codexTypeSessionMeta:
6668
return b.handleSessionMeta(payload)
69+
case codexTypeTurnContext:
70+
b.currentModel = payload.Get("model").Str
6771
case codexTypeResponseItem:
6872
b.handleResponseItem(payload, ts)
6973
}
@@ -125,6 +129,7 @@ func (b *codexSessionBuilder) handleResponseItem(
125129
Content: content,
126130
Timestamp: ts,
127131
ContentLength: len(content),
132+
Model: b.currentModel,
128133
})
129134
b.ordinal++
130135
}
@@ -147,6 +152,7 @@ func (b *codexSessionBuilder) handleFunctionCall(
147152
Timestamp: ts,
148153
HasToolUse: true,
149154
ContentLength: len(content),
155+
Model: b.currentModel,
150156
ToolCalls: []ParsedToolCall{{
151157
ToolName: name,
152158
Category: NormalizeToolCategory(name),

internal/parser/codex_parser_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,70 @@ func TestParseCodexSession_InputJSON(t *testing.T) {
278278
})
279279
}
280280

281+
func TestParseCodexSession_TurnContextModel(t *testing.T) {
282+
t.Run("model from turn_context applied to subsequent messages", func(t *testing.T) {
283+
content := testjsonl.JoinJSONL(
284+
testjsonl.CodexSessionMetaJSON("m-1", "/tmp", "user", tsEarly),
285+
testjsonl.CodexTurnContextJSON("gpt-5-codex", tsEarlyS1),
286+
testjsonl.CodexMsgJSON("user", "hello", tsEarlyS1),
287+
testjsonl.CodexMsgJSON("assistant", "hi there", tsEarlyS5),
288+
)
289+
sess, msgs := runCodexParserTest(t, "test.jsonl", content, false)
290+
require.NotNil(t, sess)
291+
assert.Equal(t, 2, len(msgs))
292+
assert.Equal(t, "gpt-5-codex", msgs[0].Model)
293+
assert.Equal(t, "gpt-5-codex", msgs[1].Model)
294+
})
295+
296+
t.Run("model changes across turns", func(t *testing.T) {
297+
content := testjsonl.JoinJSONL(
298+
testjsonl.CodexSessionMetaJSON("m-2", "/tmp", "user", tsEarly),
299+
testjsonl.CodexTurnContextJSON("gpt-5-codex", tsEarlyS1),
300+
testjsonl.CodexMsgJSON("user", "hello", tsEarlyS1),
301+
testjsonl.CodexMsgJSON("assistant", "hi", tsEarlyS5),
302+
testjsonl.CodexTurnContextJSON("o3-pro", tsLate),
303+
testjsonl.CodexMsgJSON("user", "think harder", tsLate),
304+
testjsonl.CodexMsgJSON("assistant", "deep thought", tsLateS5),
305+
)
306+
_, msgs := runCodexParserTest(t, "test.jsonl", content, false)
307+
assert.Equal(t, 4, len(msgs))
308+
assert.Equal(t, "gpt-5-codex", msgs[0].Model)
309+
assert.Equal(t, "gpt-5-codex", msgs[1].Model)
310+
assert.Equal(t, "o3-pro", msgs[2].Model)
311+
assert.Equal(t, "o3-pro", msgs[3].Model)
312+
})
313+
314+
t.Run("empty model in turn_context clears previous model", func(t *testing.T) {
315+
content := testjsonl.JoinJSONL(
316+
testjsonl.CodexSessionMetaJSON("m-4", "/tmp", "user", tsEarly),
317+
testjsonl.CodexTurnContextJSON("gpt-5-codex", tsEarlyS1),
318+
testjsonl.CodexMsgJSON("user", "hello", tsEarlyS1),
319+
testjsonl.CodexMsgJSON("assistant", "hi", tsEarlyS5),
320+
testjsonl.CodexTurnContextJSON("", tsLate),
321+
testjsonl.CodexMsgJSON("user", "follow up", tsLate),
322+
testjsonl.CodexMsgJSON("assistant", "reply", tsLateS5),
323+
)
324+
_, msgs := runCodexParserTest(t, "test.jsonl", content, false)
325+
assert.Equal(t, 4, len(msgs))
326+
assert.Equal(t, "gpt-5-codex", msgs[0].Model)
327+
assert.Equal(t, "gpt-5-codex", msgs[1].Model)
328+
assert.Empty(t, msgs[2].Model)
329+
assert.Empty(t, msgs[3].Model)
330+
})
331+
332+
t.Run("no turn_context leaves model empty", func(t *testing.T) {
333+
content := testjsonl.JoinJSONL(
334+
testjsonl.CodexSessionMetaJSON("m-3", "/tmp", "user", tsEarly),
335+
testjsonl.CodexMsgJSON("user", "hello", tsEarlyS1),
336+
testjsonl.CodexMsgJSON("assistant", "hi", tsEarlyS5),
337+
)
338+
_, msgs := runCodexParserTest(t, "test.jsonl", content, false)
339+
assert.Equal(t, 2, len(msgs))
340+
assert.Empty(t, msgs[0].Model)
341+
assert.Empty(t, msgs[1].Model)
342+
})
343+
}
344+
281345
func TestParseCodexSession_EdgeCases(t *testing.T) {
282346
t.Run("skips system messages", func(t *testing.T) {
283347
content := testjsonl.JoinJSONL(

internal/testjsonl/testjsonl.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,20 @@ func CodexFunctionCallFieldsJSON(
217217
return mustMarshal(m)
218218
}
219219

220+
// CodexTurnContextJSON returns a Codex turn_context entry as a
221+
// JSON string with the given model.
222+
func CodexTurnContextJSON(model, timestamp string) string {
223+
m := map[string]any{
224+
"type": "turn_context",
225+
"timestamp": timestamp,
226+
"payload": map[string]any{
227+
"model": model,
228+
"cwd": "/tmp",
229+
},
230+
}
231+
return mustMarshal(m)
232+
}
233+
220234
// ClaudeEntryJSON returns a Claude JSONL entry with uuid and
221235
// parentUuid fields.
222236
func ClaudeEntryJSON(

0 commit comments

Comments
 (0)