Skip to content

Commit f788ff9

Browse files
committed
feat: add model tracking to CSV exports
- Add ModelInfo type for tracking provider/model information - Extract model from AssistantMessage events via message.updated hook - Add model field to SessionData and CsvEntryData - Add setModel method to SessionManager - Extend CSV header with model column - Format model as providerID/modelID (e.g., anthropic/claude-opus-4) This enables per-model token cost calculation in timesheet reports. BREAKING CHANGE: CSV format now includes 'model' as the 16th column. Existing CSV files need migration (add model column to header and data rows).
1 parent a844018 commit f788ff9

7 files changed

Lines changed: 100 additions & 6 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@techdivision/opencode-time-tracking",
3-
"version": "0.1.9",
3+
"version": "0.2.0",
44
"description": "Automatic time tracking plugin for OpenCode - tracks session duration and tool usage to CSV",
55
"main": "src/Plugin.ts",
66
"types": "src/Plugin.ts",

src/hooks/EventHook.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @fileoverview Event hook for session lifecycle and token tracking.
33
*/
44

5-
import type { Event } from "@opencode-ai/sdk"
5+
import type { AssistantMessage, Event, Message } from "@opencode-ai/sdk"
66

77
import type { CsvWriter } from "../services/CsvWriter"
88
import type { SessionManager } from "../services/SessionManager"
@@ -12,6 +12,13 @@ import type { OpencodeClient } from "../types/OpencodeClient"
1212

1313
import { DescriptionGenerator } from "../utils/DescriptionGenerator"
1414

15+
/**
16+
* Properties for message.updated events.
17+
*/
18+
interface MessageUpdatedProperties {
19+
info: Message
20+
}
21+
1522
/**
1623
* Extracts the summary title from the last user message.
1724
*
@@ -60,10 +67,11 @@ async function extractSummaryTitle(
6067
* @returns The event hook function
6168
*
6269
* @remarks
63-
* Handles two types of events:
70+
* Handles three types of events:
6471
*
65-
* 1. **message.part.updated** - Tracks token usage from step-finish parts
66-
* 2. **session.idle** - Finalizes and exports the session
72+
* 1. **message.updated** - Tracks model from assistant messages
73+
* 2. **message.part.updated** - Tracks token usage from step-finish parts
74+
* 3. **session.idle** - Finalizes and exports the session
6775
*
6876
* @example
6977
* ```typescript
@@ -78,6 +86,25 @@ export function createEventHook(
7886
client: OpencodeClient
7987
) {
8088
return async ({ event }: { event: Event }): Promise<void> => {
89+
// Track model from assistant messages
90+
if (event.type === "message.updated") {
91+
const props = event.properties as MessageUpdatedProperties
92+
const message = props.info
93+
94+
if (message.role === "assistant") {
95+
const assistantMsg = message as AssistantMessage
96+
97+
if (assistantMsg.modelID && assistantMsg.providerID) {
98+
sessionManager.setModel(assistantMsg.sessionID, {
99+
modelID: assistantMsg.modelID,
100+
providerID: assistantMsg.providerID,
101+
})
102+
}
103+
}
104+
105+
return
106+
}
107+
81108
// Track token usage from step-finish events
82109
if (event.type === "message.part.updated") {
83110
const props = event.properties as MessagePartUpdatedProperties
@@ -129,6 +156,11 @@ export function createEventHook(
129156
session.tokenUsage.output +
130157
session.tokenUsage.reasoning
131158

159+
// Format model as providerID/modelID
160+
const modelString = session.model
161+
? `${session.model.providerID}/${session.model.modelID}`
162+
: null
163+
132164
try {
133165
await csvWriter.write({
134166
ticket: session.ticket,
@@ -138,6 +170,7 @@ export function createEventHook(
138170
description,
139171
notes: `Auto-tracked: ${toolSummary}`,
140172
tokenUsage: session.tokenUsage,
173+
model: modelString,
141174
})
142175

143176
const minutes = Math.round(durationSeconds / 60)

src/services/CsvWriter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import "../types/Bun"
1818
* Compatible with Jira/Tempo time tracking import.
1919
*/
2020
const CSV_HEADER =
21-
"id,start_date,end_date,user,ticket_name,issue_key,account_key,start_time,end_time,duration_seconds,tokens_used,tokens_remaining,story_points,description,notes"
21+
"id,start_date,end_date,user,ticket_name,issue_key,account_key,start_time,end_time,duration_seconds,tokens_used,tokens_remaining,story_points,description,notes,model"
2222

2323
/**
2424
* Writes time tracking entries to a CSV file.
@@ -122,6 +122,7 @@ export class CsvWriter {
122122
"",
123123
CsvFormatter.escape(data.description),
124124
CsvFormatter.escape(data.notes),
125+
data.model ?? "",
125126
]
126127

127128
const csvLine = fields.map((f) => `"${f}"`).join(",")

src/services/SessionManager.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import type { ActivityData } from "../types/ActivityData"
6+
import type { ModelInfo } from "../types/ModelInfo"
67
import type { SessionData } from "../types/SessionData"
78
import type { TokenUsage } from "../types/TokenUsage"
89

@@ -61,6 +62,7 @@ export class SessionManager {
6162
cacheRead: 0,
6263
cacheWrite: 0,
6364
},
65+
model: null,
6466
}
6567

6668
this.sessions.set(sessionID, session)
@@ -126,4 +128,22 @@ export class SessionManager {
126128
session.ticket = ticket
127129
}
128130
}
131+
132+
/**
133+
* Sets the model for a session.
134+
*
135+
* @param sessionID - The OpenCode session identifier
136+
* @param model - The model information
137+
*
138+
* @remarks
139+
* Only sets the model if it hasn't been set yet.
140+
* The first model detected in a session is used.
141+
*/
142+
setModel(sessionID: string, model: ModelInfo): void {
143+
const session = this.sessions.get(sessionID)
144+
145+
if (session && !session.model) {
146+
session.model = model
147+
}
148+
}
129149
}

src/types/CsvEntryData.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,12 @@ export interface CsvEntryData {
2828

2929
/** Token consumption statistics */
3030
tokenUsage: TokenUsage
31+
32+
/**
33+
* Model identifier in format `providerID/modelID`.
34+
*
35+
* @remarks
36+
* Examples: `anthropic/claude-opus-4`, `openai/gpt-5`
37+
*/
38+
model: string | null
3139
}

src/types/ModelInfo.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* @fileoverview Model information type for tracking which LLM was used.
3+
*/
4+
5+
/**
6+
* Information about the model used in a session.
7+
*
8+
* @remarks
9+
* Extracted from AssistantMessage events in the OpenCode SDK.
10+
* Used to calculate token costs per model.
11+
*/
12+
export interface ModelInfo {
13+
/**
14+
* Model identifier (e.g., "claude-opus-4", "gpt-5").
15+
*
16+
* @remarks
17+
* This is the model name as reported by the provider.
18+
*/
19+
modelID: string
20+
21+
/**
22+
* Provider identifier (e.g., "anthropic", "openai").
23+
*
24+
* @remarks
25+
* Combined with modelID to form the full model reference: `providerID/modelID`
26+
*/
27+
providerID: string
28+
}

src/types/SessionData.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import type { ActivityData } from "./ActivityData"
6+
import type { ModelInfo } from "./ModelInfo"
67
import type { TokenUsage } from "./TokenUsage"
78

89
/**
@@ -20,4 +21,7 @@ export interface SessionData {
2021

2122
/** Cumulative token usage for the session */
2223
tokenUsage: TokenUsage
24+
25+
/** Model used in this session, or `null` if not detected */
26+
model: ModelInfo | null
2327
}

0 commit comments

Comments
 (0)