oh-my-opencode-dashboard 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import * as fs from "node:fs"
2
1
  import * as os from "node:os"
3
2
  import * as path from "node:path"
4
- import { describe, expect, it } from "vitest"
3
+ import * as fs from "node:fs"
4
+ import { describe, expect, it, vi } from "vitest"
5
5
  import { deriveBackgroundTasks } from "./background-tasks"
6
6
  import { getStorageRoots } from "./session"
7
7
 
@@ -602,6 +602,7 @@ describe("deriveBackgroundTasks", () => {
602
602
  expect(rows[0].toolCalls).toBe(0) // No tool calls in fallback session
603
603
  expect(rows[0].lastTool).toBe(null)
604
604
  expect(rows[0].status).toBe("unknown") // Should be unknown since session exists but no tool calls
605
+ expect(rows[0].timeline).toBe("") // Unknown status should emit no timeline
605
606
  })
606
607
 
607
608
  it("links sync delegate_task rows to Background sessions when forced-to-background but waited", () => {
@@ -704,4 +705,296 @@ describe("deriveBackgroundTasks", () => {
704
705
  expect((rows[0] as unknown as Record<string, unknown>).input).toBeUndefined()
705
706
  expect((rows[0] as unknown as Record<string, unknown>).state).toBeUndefined()
706
707
  })
708
+
709
+ it("derives lastModel from background session assistant metas", () => {
710
+ // #given
711
+ const storageRoot = mkStorageRoot()
712
+ const storage = getStorageRoots(storageRoot)
713
+ const mainSessionId = "ses_main"
714
+
715
+ const msgDir = path.join(storage.message, mainSessionId)
716
+ fs.mkdirSync(msgDir, { recursive: true })
717
+ const messageID = "msg_1"
718
+ fs.writeFileSync(
719
+ path.join(msgDir, `${messageID}.json`),
720
+ JSON.stringify({
721
+ id: messageID,
722
+ sessionID: mainSessionId,
723
+ role: "assistant",
724
+ time: { created: 1000 },
725
+ }),
726
+ "utf8"
727
+ )
728
+ const partDir = path.join(storage.part, messageID)
729
+ fs.mkdirSync(partDir, { recursive: true })
730
+ fs.writeFileSync(
731
+ path.join(partDir, "part_1.json"),
732
+ JSON.stringify({
733
+ id: "part_1",
734
+ sessionID: mainSessionId,
735
+ messageID,
736
+ type: "tool",
737
+ callID: "call_1",
738
+ tool: "delegate_task",
739
+ state: {
740
+ status: "completed",
741
+ input: {
742
+ run_in_background: true,
743
+ description: "Model scan",
744
+ subagent_type: "explore",
745
+ },
746
+ },
747
+ }),
748
+ "utf8"
749
+ )
750
+
751
+ const projectID = "proj"
752
+ const sessDir = path.join(storage.session, projectID)
753
+ fs.mkdirSync(sessDir, { recursive: true })
754
+ fs.writeFileSync(
755
+ path.join(sessDir, "ses_child.json"),
756
+ JSON.stringify({
757
+ id: "ses_child",
758
+ projectID,
759
+ directory: "/tmp/project",
760
+ title: "Background: Model scan",
761
+ parentID: mainSessionId,
762
+ time: { created: 1500, updated: 1500 },
763
+ }),
764
+ "utf8"
765
+ )
766
+
767
+ const childMsgDir = path.join(storage.message, "ses_child")
768
+ fs.mkdirSync(childMsgDir, { recursive: true })
769
+ const childMsgId = "msg_child"
770
+ fs.writeFileSync(
771
+ path.join(childMsgDir, `${childMsgId}.json`),
772
+ JSON.stringify({
773
+ id: childMsgId,
774
+ sessionID: "ses_child",
775
+ role: "assistant",
776
+ time: { created: 2000 },
777
+ providerID: "openai",
778
+ modelID: "gpt-5.2",
779
+ }),
780
+ "utf8"
781
+ )
782
+
783
+ // #when
784
+ const rows = deriveBackgroundTasks({ storage, mainSessionId })
785
+
786
+ // #then
787
+ expect(rows.length).toBe(1)
788
+ expect(rows[0].lastModel).toBe("openai/gpt-5.2")
789
+ })
790
+
791
+ it("orders lastModel by time.created over file mtime", () => {
792
+ // #given
793
+ const storageRoot = mkStorageRoot()
794
+ const storage = getStorageRoots(storageRoot)
795
+ const mainSessionId = "ses_main"
796
+
797
+ const msgDir = path.join(storage.message, mainSessionId)
798
+ fs.mkdirSync(msgDir, { recursive: true })
799
+ const messageID = "msg_1"
800
+ fs.writeFileSync(
801
+ path.join(msgDir, `${messageID}.json`),
802
+ JSON.stringify({
803
+ id: messageID,
804
+ sessionID: mainSessionId,
805
+ role: "assistant",
806
+ time: { created: 1000 },
807
+ }),
808
+ "utf8"
809
+ )
810
+ const partDir = path.join(storage.part, messageID)
811
+ fs.mkdirSync(partDir, { recursive: true })
812
+ fs.writeFileSync(
813
+ path.join(partDir, "part_1.json"),
814
+ JSON.stringify({
815
+ id: "part_1",
816
+ sessionID: mainSessionId,
817
+ messageID,
818
+ type: "tool",
819
+ callID: "call_1",
820
+ tool: "delegate_task",
821
+ state: {
822
+ status: "completed",
823
+ input: {
824
+ run_in_background: true,
825
+ description: "Ordering scan",
826
+ subagent_type: "explore",
827
+ },
828
+ },
829
+ }),
830
+ "utf8"
831
+ )
832
+
833
+ const projectID = "proj"
834
+ const sessDir = path.join(storage.session, projectID)
835
+ fs.mkdirSync(sessDir, { recursive: true })
836
+ fs.writeFileSync(
837
+ path.join(sessDir, "ses_child.json"),
838
+ JSON.stringify({
839
+ id: "ses_child",
840
+ projectID,
841
+ directory: "/tmp/project",
842
+ title: "Background: Ordering scan",
843
+ parentID: mainSessionId,
844
+ time: { created: 1500, updated: 1500 },
845
+ }),
846
+ "utf8"
847
+ )
848
+
849
+ const childMsgDir = path.join(storage.message, "ses_child")
850
+ fs.mkdirSync(childMsgDir, { recursive: true })
851
+ const olderMsgId = "msg_older"
852
+ const newerMsgId = "msg_newer"
853
+
854
+ fs.writeFileSync(
855
+ path.join(childMsgDir, `${newerMsgId}.json`),
856
+ JSON.stringify({
857
+ id: newerMsgId,
858
+ sessionID: "ses_child",
859
+ role: "assistant",
860
+ time: { created: 3000 },
861
+ providerID: "openai",
862
+ modelID: "gpt-newer",
863
+ }),
864
+ "utf8"
865
+ )
866
+ fs.writeFileSync(
867
+ path.join(childMsgDir, `${olderMsgId}.json`),
868
+ JSON.stringify({
869
+ id: olderMsgId,
870
+ sessionID: "ses_child",
871
+ role: "assistant",
872
+ time: { created: 1000 },
873
+ providerID: "openai",
874
+ modelID: "gpt-older",
875
+ }),
876
+ "utf8"
877
+ )
878
+
879
+ // #when
880
+ const rows = deriveBackgroundTasks({ storage, mainSessionId })
881
+
882
+ // #then
883
+ expect(rows.length).toBe(1)
884
+ expect(rows[0].lastModel).toBe("openai/gpt-newer")
885
+ })
886
+
887
+ it("memoizes background session reads for shared sessionId", () => {
888
+ // #given
889
+ const storageRoot = mkStorageRoot()
890
+ const storage = getStorageRoots(storageRoot)
891
+ const mainSessionId = "ses_main"
892
+
893
+ // Clear any previous mock state
894
+ vi.clearAllMocks()
895
+
896
+ const msgDir = path.join(storage.message, mainSessionId)
897
+ fs.mkdirSync(msgDir, { recursive: true })
898
+ const messageID = "msg_1"
899
+ fs.writeFileSync(
900
+ path.join(msgDir, `${messageID}.json`),
901
+ JSON.stringify({
902
+ id: messageID,
903
+ sessionID: mainSessionId,
904
+ role: "assistant",
905
+ time: { created: 1000 },
906
+ }),
907
+ "utf8"
908
+ )
909
+ const partDir = path.join(storage.part, messageID)
910
+ fs.mkdirSync(partDir, { recursive: true })
911
+ fs.writeFileSync(
912
+ path.join(partDir, "part_1.json"),
913
+ JSON.stringify({
914
+ id: "part_1",
915
+ sessionID: mainSessionId,
916
+ messageID,
917
+ type: "tool",
918
+ callID: "call_1",
919
+ tool: "delegate_task",
920
+ state: {
921
+ status: "completed",
922
+ input: {
923
+ run_in_background: true,
924
+ description: "Memo scan",
925
+ subagent_type: "explore",
926
+ },
927
+ },
928
+ }),
929
+ "utf8"
930
+ )
931
+ fs.writeFileSync(
932
+ path.join(partDir, "part_2.json"),
933
+ JSON.stringify({
934
+ id: "part_2",
935
+ sessionID: mainSessionId,
936
+ messageID,
937
+ type: "tool",
938
+ callID: "call_2",
939
+ tool: "delegate_task",
940
+ state: {
941
+ status: "completed",
942
+ input: {
943
+ run_in_background: true,
944
+ description: "Memo scan",
945
+ subagent_type: "explore",
946
+ },
947
+ },
948
+ }),
949
+ "utf8"
950
+ )
951
+
952
+ const projectID = "proj"
953
+ const sessDir = path.join(storage.session, projectID)
954
+ fs.mkdirSync(sessDir, { recursive: true })
955
+ fs.writeFileSync(
956
+ path.join(sessDir, "ses_child.json"),
957
+ JSON.stringify({
958
+ id: "ses_child",
959
+ projectID,
960
+ directory: "/tmp/project",
961
+ title: "Background: Memo scan",
962
+ parentID: mainSessionId,
963
+ time: { created: 1500, updated: 1500 },
964
+ }),
965
+ "utf8"
966
+ )
967
+
968
+ const childMsgDir = path.join(storage.message, "ses_child")
969
+ fs.mkdirSync(childMsgDir, { recursive: true })
970
+ const childMsgId = "msg_child"
971
+ fs.writeFileSync(
972
+ path.join(childMsgDir, `${childMsgId}.json`),
973
+ JSON.stringify({
974
+ id: childMsgId,
975
+ sessionID: "ses_child",
976
+ role: "assistant",
977
+ time: { created: 2000 },
978
+ providerID: "openai",
979
+ modelID: "gpt-5.2",
980
+ }),
981
+ "utf8"
982
+ )
983
+
984
+ const readdirSync = vi.fn(fs.readdirSync)
985
+ const fsLike = {
986
+ readFileSync: fs.readFileSync,
987
+ readdirSync,
988
+ existsSync: fs.existsSync,
989
+ statSync: fs.statSync,
990
+ }
991
+
992
+ // #when
993
+ const rows = deriveBackgroundTasks({ storage, mainSessionId, fs: fsLike })
994
+
995
+ // #then
996
+ const backgroundReads = readdirSync.mock.calls.filter((call) => call[0] === childMsgDir)
997
+ expect(rows.length).toBe(2)
998
+ expect(backgroundReads.length).toBe(1)
999
+ })
707
1000
  })
@@ -2,6 +2,9 @@ import * as fs from "node:fs"
2
2
  import * as path from "node:path"
3
3
  import type { OpenCodeStorageRoots, SessionMetadata, StoredMessageMeta, StoredToolPart } from "./session"
4
4
  import { getMessageDir } from "./session"
5
+ import { pickLatestModelString } from "./model"
6
+
7
+ type FsLike = Pick<typeof fs, "readFileSync" | "readdirSync" | "existsSync" | "statSync">
5
8
 
6
9
  export type BackgroundTaskRow = {
7
10
  id: string
@@ -10,6 +13,7 @@ export type BackgroundTaskRow = {
10
13
  status: "queued" | "running" | "completed" | "error" | "unknown"
11
14
  toolCalls: number | null
12
15
  lastTool: string | null
16
+ lastModel: string | null
13
17
  timeline: string
14
18
  sessionId: string | null
15
19
  }
@@ -24,31 +28,31 @@ function clampString(value: unknown, maxLen: number): string | null {
24
28
  return s.length <= maxLen ? s : s.slice(0, maxLen)
25
29
  }
26
30
 
27
- function readJsonFile<T>(filePath: string): T | null {
31
+ function readJsonFile<T>(filePath: string, fsLike: FsLike): T | null {
28
32
  try {
29
- const content = fs.readFileSync(filePath, "utf8")
33
+ const content = fsLike.readFileSync(filePath, "utf8")
30
34
  return JSON.parse(content) as T
31
35
  } catch {
32
36
  return null
33
37
  }
34
38
  }
35
39
 
36
- function listJsonFiles(dir: string): string[] {
40
+ function listJsonFiles(dir: string, fsLike: FsLike): string[] {
37
41
  try {
38
- return fs.readdirSync(dir).filter((f) => f.endsWith(".json"))
42
+ return fsLike.readdirSync(dir).filter((f) => f.endsWith(".json"))
39
43
  } catch {
40
44
  return []
41
45
  }
42
46
  }
43
47
 
44
- function readToolPartsForMessage(storage: OpenCodeStorageRoots, messageID: string): StoredToolPart[] {
48
+ function readToolPartsForMessage(storage: OpenCodeStorageRoots, messageID: string, fsLike: FsLike): StoredToolPart[] {
45
49
  const partDir = path.join(storage.part, messageID)
46
- if (!fs.existsSync(partDir)) return []
50
+ if (!fsLike.existsSync(partDir)) return []
47
51
 
48
- const files = listJsonFiles(partDir).sort()
52
+ const files = listJsonFiles(partDir, fsLike).sort()
49
53
  const parts: StoredToolPart[] = []
50
54
  for (const f of files) {
51
- const p = readJsonFile<StoredToolPart>(path.join(partDir, f))
55
+ const p = readJsonFile<StoredToolPart>(path.join(partDir, f), fsLike)
52
56
  if (p && p.type === "tool" && typeof p.tool === "string" && p.state && typeof p.state === "object") {
53
57
  parts.push(p)
54
58
  }
@@ -56,14 +60,14 @@ function readToolPartsForMessage(storage: OpenCodeStorageRoots, messageID: strin
56
60
  return parts
57
61
  }
58
62
 
59
- function readRecentMessageMetas(messageDir: string, maxMessages: number): StoredMessageMeta[] {
60
- if (!messageDir || !fs.existsSync(messageDir)) return []
61
- const files = listJsonFiles(messageDir)
63
+ function readRecentMessageMetas(messageDir: string, maxMessages: number, fsLike: FsLike): StoredMessageMeta[] {
64
+ if (!messageDir || !fsLike.existsSync(messageDir)) return []
65
+ const files = listJsonFiles(messageDir, fsLike)
62
66
  .map((f) => ({
63
67
  f,
64
68
  mtime: (() => {
65
69
  try {
66
- return fs.statSync(path.join(messageDir, f)).mtimeMs
70
+ return fsLike.statSync(path.join(messageDir, f)).mtimeMs
67
71
  } catch {
68
72
  return 0
69
73
  }
@@ -74,22 +78,22 @@ function readRecentMessageMetas(messageDir: string, maxMessages: number): Stored
74
78
 
75
79
  const metas: StoredMessageMeta[] = []
76
80
  for (const item of files) {
77
- const meta = readJsonFile<StoredMessageMeta>(path.join(messageDir, item.f))
81
+ const meta = readJsonFile<StoredMessageMeta>(path.join(messageDir, item.f), fsLike)
78
82
  if (meta && typeof meta.id === "string") metas.push(meta)
79
83
  }
80
84
  return metas
81
85
  }
82
86
 
83
- export function readAllSessionMetas(sessionStorage: string): SessionMetadata[] {
84
- if (!fs.existsSync(sessionStorage)) return []
87
+ export function readAllSessionMetas(sessionStorage: string, fsLike: FsLike = fs): SessionMetadata[] {
88
+ if (!fsLike.existsSync(sessionStorage)) return []
85
89
  const metas: SessionMetadata[] = []
86
90
  try {
87
- const projectDirs = fs.readdirSync(sessionStorage, { withFileTypes: true })
91
+ const projectDirs = fsLike.readdirSync(sessionStorage, { withFileTypes: true })
88
92
  for (const d of projectDirs) {
89
93
  if (!d.isDirectory()) continue
90
94
  const projectPath = path.join(sessionStorage, d.name)
91
- for (const file of listJsonFiles(projectPath)) {
92
- const meta = readJsonFile<SessionMetadata>(path.join(projectPath, file))
95
+ for (const file of listJsonFiles(projectPath, fsLike)) {
96
+ const meta = readJsonFile<SessionMetadata>(path.join(projectPath, file), fsLike)
93
97
  if (meta && typeof meta.id === "string") metas.push(meta)
94
98
  }
95
99
  }
@@ -152,9 +156,11 @@ function findTaskSessionId(opts: {
152
156
  return candidates[0]?.id ?? null
153
157
  }
154
158
 
155
- function deriveBackgroundSessionStats(storage: OpenCodeStorageRoots, sessionId: string): { toolCalls: number; lastTool: string | null; lastUpdateAt: number | null } {
156
- const messageDir = getMessageDir(storage.message, sessionId)
157
- const metas = readRecentMessageMetas(messageDir, 200)
159
+ function deriveBackgroundSessionStats(
160
+ storage: OpenCodeStorageRoots,
161
+ metas: StoredMessageMeta[],
162
+ fsLike: FsLike
163
+ ): { toolCalls: number; lastTool: string | null; lastUpdateAt: number | null } {
158
164
  let toolCalls = 0
159
165
  let lastTool: string | null = null
160
166
  let lastUpdateAt: number | null = null
@@ -170,7 +176,7 @@ function deriveBackgroundSessionStats(storage: OpenCodeStorageRoots, sessionId:
170
176
  for (const meta of ordered) {
171
177
  const created = meta.time?.created
172
178
  if (typeof created === "number") lastUpdateAt = created
173
- const parts = readToolPartsForMessage(storage, meta.id)
179
+ const parts = readToolPartsForMessage(storage, meta.id, fsLike)
174
180
  for (const p of parts) {
175
181
  toolCalls += 1
176
182
  lastTool = p.tool
@@ -211,11 +217,42 @@ export function deriveBackgroundTasks(opts: {
211
217
  storage: OpenCodeStorageRoots
212
218
  mainSessionId: string
213
219
  nowMs?: number
220
+ fs?: FsLike
214
221
  }): BackgroundTaskRow[] {
222
+ const fsLike: FsLike = opts.fs ?? fs
215
223
  const nowMs = opts.nowMs ?? Date.now()
216
224
  const messageDir = getMessageDir(opts.storage.message, opts.mainSessionId)
217
- const metas = readRecentMessageMetas(messageDir, 200)
218
- const allSessionMetas = readAllSessionMetas(opts.storage.session)
225
+ const metas = readRecentMessageMetas(messageDir, 200, fsLike)
226
+ const allSessionMetas = readAllSessionMetas(opts.storage.session, fsLike)
227
+ const backgroundMessageCache = new Map<string, StoredMessageMeta[]>()
228
+ const backgroundStatsCache = new Map<string, { toolCalls: number; lastTool: string | null; lastUpdateAt: number | null }>()
229
+ const backgroundModelCache = new Map<string, string | null>()
230
+
231
+ const readBackgroundMetas = (sessionId: string): StoredMessageMeta[] => {
232
+ const cached = backgroundMessageCache.get(sessionId)
233
+ if (cached) return cached
234
+ const backgroundMessageDir = getMessageDir(opts.storage.message, sessionId)
235
+ const recent = readRecentMessageMetas(backgroundMessageDir, 200, fsLike)
236
+ backgroundMessageCache.set(sessionId, recent)
237
+ return recent
238
+ }
239
+
240
+ const readBackgroundStats = (sessionId: string) => {
241
+ const cached = backgroundStatsCache.get(sessionId)
242
+ if (cached) return cached
243
+ const recent = readBackgroundMetas(sessionId)
244
+ const stats = deriveBackgroundSessionStats(opts.storage, recent, fsLike)
245
+ backgroundStatsCache.set(sessionId, stats)
246
+ return stats
247
+ }
248
+
249
+ const readBackgroundModel = (sessionId: string): string | null => {
250
+ if (backgroundModelCache.has(sessionId)) return backgroundModelCache.get(sessionId) ?? null
251
+ const recent = readBackgroundMetas(sessionId)
252
+ const model = pickLatestModelString(recent as unknown[])
253
+ backgroundModelCache.set(sessionId, model)
254
+ return model
255
+ }
219
256
 
220
257
  const rows: BackgroundTaskRow[] = []
221
258
 
@@ -225,7 +262,7 @@ export function deriveBackgroundTasks(opts: {
225
262
  const startedAt = meta.time?.created ?? null
226
263
  if (typeof startedAt !== "number") continue
227
264
 
228
- const parts = readToolPartsForMessage(opts.storage, meta.id)
265
+ const parts = readToolPartsForMessage(opts.storage, meta.id, fsLike)
229
266
  for (const part of parts) {
230
267
  if (part.tool !== "delegate_task") continue
231
268
  if (!part.state || typeof part.state !== "object") continue
@@ -258,7 +295,7 @@ export function deriveBackgroundTasks(opts: {
258
295
  if (typeof resume === "string" && resume.trim() !== "") {
259
296
  // Check if resumed session exists (has readable messages dir)
260
297
  const resumeMessageDir = getMessageDir(opts.storage.message, resume.trim())
261
- if (fs.existsSync(resumeMessageDir) && fs.readdirSync(resumeMessageDir).length > 0) {
298
+ if (fsLike.existsSync(resumeMessageDir) && fsLike.readdirSync(resumeMessageDir).length > 0) {
262
299
  backgroundSessionId = resume.trim()
263
300
  }
264
301
  }
@@ -283,8 +320,9 @@ export function deriveBackgroundTasks(opts: {
283
320
  }
284
321
 
285
322
  const stats = backgroundSessionId
286
- ? deriveBackgroundSessionStats(opts.storage, backgroundSessionId)
323
+ ? readBackgroundStats(backgroundSessionId)
287
324
  : { toolCalls: 0, lastTool: null, lastUpdateAt: startedAt }
325
+ const lastModel = backgroundSessionId ? readBackgroundModel(backgroundSessionId) : null
288
326
 
289
327
  // Best-effort status: if background session exists and has any tool calls, treat as running unless idle.
290
328
  let status: BackgroundTaskRow["status"] = "unknown"
@@ -305,7 +343,8 @@ export function deriveBackgroundTasks(opts: {
305
343
  status,
306
344
  toolCalls: backgroundSessionId ? stats.toolCalls : null,
307
345
  lastTool: stats.lastTool,
308
- timeline: formatTimeline(startedAt, timelineEndMs),
346
+ lastModel,
347
+ timeline: status === "unknown" ? "" : formatTimeline(startedAt, timelineEndMs),
309
348
  sessionId: backgroundSessionId,
310
349
  })
311
350
  }
@@ -0,0 +1,79 @@
1
+ type ModelParts = {
2
+ providerID?: string
3
+ modelID?: string
4
+ }
5
+
6
+ function isRecord(value: unknown): value is Record<string, unknown> {
7
+ return typeof value === "object" && value !== null
8
+ }
9
+
10
+ function readString(value: unknown): string | null {
11
+ if (typeof value !== "string") return null
12
+ const trimmed = value.trim()
13
+ return trimmed.length > 0 ? trimmed : null
14
+ }
15
+
16
+ function readModelParts(value: Record<string, unknown>): ModelParts {
17
+ const providerID =
18
+ readString(value.providerID) ??
19
+ readString(value.providerId) ??
20
+ readString(value.provider_id)
21
+ const modelID =
22
+ readString(value.modelID) ??
23
+ readString(value.modelId) ??
24
+ readString(value.model_id)
25
+
26
+ return { providerID: providerID ?? undefined, modelID: modelID ?? undefined }
27
+ }
28
+
29
+ export function extractModelString(meta: unknown): string | null {
30
+ if (!isRecord(meta)) return null
31
+
32
+ const direct = readModelParts(meta)
33
+ if (direct.providerID && direct.modelID) return `${direct.providerID}/${direct.modelID}`
34
+
35
+ const nested = meta.model
36
+ if (isRecord(nested)) {
37
+ const nestedParts = readModelParts(nested)
38
+ if (nestedParts.providerID && nestedParts.modelID) {
39
+ return `${nestedParts.providerID}/${nestedParts.modelID}`
40
+ }
41
+ }
42
+
43
+ return null
44
+ }
45
+
46
+ type ModelCandidate = {
47
+ created: number
48
+ id: string
49
+ role: string | null
50
+ model: string
51
+ }
52
+
53
+ export function pickLatestModelString(metas: Array<unknown>): string | null {
54
+ const candidates: ModelCandidate[] = []
55
+
56
+ for (const meta of metas) {
57
+ if (!isRecord(meta)) continue
58
+ const model = extractModelString(meta)
59
+ if (!model) continue
60
+
61
+ const created = typeof meta.time === "object" && meta.time !== null && typeof (meta.time as { created?: unknown }).created === "number"
62
+ ? (meta.time as { created: number }).created
63
+ : 0
64
+ const id = readString(meta.id) ?? ""
65
+ const role = readString(meta.role)
66
+
67
+ candidates.push({ created, id, role, model })
68
+ }
69
+
70
+ if (candidates.length === 0) return null
71
+
72
+ candidates.sort((a, b) => {
73
+ if (b.created !== a.created) return b.created - a.created
74
+ return b.id.localeCompare(a.id)
75
+ })
76
+
77
+ const assistant = candidates.find((candidate) => candidate.role === "assistant")
78
+ return assistant?.model ?? candidates[0]?.model ?? null
79
+ }