opencode-visual-cache 1.7.0-beta.3 → 1.7.0-beta.5
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.
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/balance-providers.js +27 -1
- package/dist/index.js +4 -1
- package/dist/tui.js +31 -2
- package/dist/v2/index.js +1 -1
- package/dist/v2/status.d.ts +3 -1
- package/dist/v2/status.js +12 -2
- package/dist/v2.js +104 -73
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/balance-providers.ts +25 -1
- package/src/core/color.ts +108 -108
- package/src/core/currency.ts +46 -46
- package/src/core/estimate.ts +36 -36
- package/src/core/format.ts +81 -81
- package/src/core/index.ts +5 -5
- package/src/core/types.ts +17 -17
- package/src/index.tsx +3 -0
- package/src/panel/panel-api.ts +104 -104
- package/src/v2/data.ts +112 -112
- package/src/v2/index.tsx +187 -187
- package/src/v2/sidebar.tsx +69 -69
- package/src/v2/status.tsx +108 -96
- package/src/v2/theme.ts +19 -19
- package/src/v2/types.ts +159 -159
- package/src/v2/v2-panel-api.ts +177 -177
- package/tui/index.js +1 -1
package/src/panel/panel-api.ts
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
import type { Message, Part, Session } from "@opencode-ai/sdk"
|
|
2
|
-
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
3
|
-
import type { BalanceEntry } from "../balance-providers"
|
|
4
|
-
import type { LangCode } from "../i18n"
|
|
5
|
-
|
|
6
|
-
/** 会话信息(面板消费的字段;SDK Session 类型过严,用宽松接口) */
|
|
7
|
-
export interface PanelSession {
|
|
8
|
-
id: string
|
|
9
|
-
title?: string
|
|
10
|
-
agent?: string
|
|
11
|
-
model?: { providerID?: string; id?: string }
|
|
12
|
-
tokens?: { input?: number; output?: number; reasoning?: number; cache?: { read?: number; write?: number } }
|
|
13
|
-
cost?: number
|
|
14
|
-
[key: string]: unknown
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Panel API 契约:TokenCachePanel 消费的宿主 API 子集。
|
|
19
|
-
* V1(TuiPluginApi)天然满足;V2(opencode2 context)由 v2-panel-api 适配实现。
|
|
20
|
-
*/
|
|
21
|
-
export interface PanelApi {
|
|
22
|
-
kv: {
|
|
23
|
-
ready: boolean
|
|
24
|
-
get<T>(key: string, fallback?: T): T | undefined
|
|
25
|
-
set(key: string, value: unknown): void | Promise<void>
|
|
26
|
-
}
|
|
27
|
-
state: {
|
|
28
|
-
session: {
|
|
29
|
-
get(id: string): PanelSession | undefined
|
|
30
|
-
/** V1 返回 sdk/v2 的 Message、V2 返回 SessionMessageInfo——统一放宽 */
|
|
31
|
-
messages(id: string): readonly any[]
|
|
32
|
-
}
|
|
33
|
-
provider: readonly Record<string, any>[]
|
|
34
|
-
config: any
|
|
35
|
-
part(messageID: string): readonly any[]
|
|
36
|
-
path: { directory: string }
|
|
37
|
-
}
|
|
38
|
-
event: {
|
|
39
|
-
on(type: string, handler: (event: unknown) => void): () => void
|
|
40
|
-
}
|
|
41
|
-
renderer: { terminalWidth: number }
|
|
42
|
-
keys: { formatBindings(binding: unknown): string | undefined }
|
|
43
|
-
tuiConfig: { keybinds: { get(command: string): unknown } }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface BalanceState {
|
|
47
|
-
status: "idle" | "loading" | "ok" | "error"
|
|
48
|
-
data: BalanceEntry[] | null
|
|
49
|
-
lastFetch: number
|
|
50
|
-
error?: string
|
|
51
|
-
key?: string // 上次成功/尝试查询所用的 key,用于检测 key 是否更换
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Signals shared between the TUI component and slash commands.
|
|
55
|
-
* Created in the `tui` function scope so they do not survive module reload —
|
|
56
|
-
* the component re-creates them on mount and restores user config from kv. */
|
|
57
|
-
export interface PanelSignals {
|
|
58
|
-
currencySymbol: () => string
|
|
59
|
-
setCurrencySymbol: (v: string) => void
|
|
60
|
-
exchangeRate: () => number
|
|
61
|
-
setExchangeRate: (v: number) => void
|
|
62
|
-
langCode: () => LangCode
|
|
63
|
-
setLangCode: (v: LangCode) => void
|
|
64
|
-
sectionDetail: () => boolean
|
|
65
|
-
setSectionDetail: (v: boolean) => void
|
|
66
|
-
sectionModel: () => boolean
|
|
67
|
-
setSectionModel: (v: boolean) => void
|
|
68
|
-
sectionDist: () => boolean
|
|
69
|
-
setSectionDist: (v: boolean) => void
|
|
70
|
-
sectionSkills: () => boolean
|
|
71
|
-
setSectionSkills: (v: boolean) => void
|
|
72
|
-
sectionBalance: () => boolean
|
|
73
|
-
setSectionBalance: (v: boolean) => void
|
|
74
|
-
/** Bottom status bar (prompt hint line) visibility. */
|
|
75
|
-
sectionBottom: () => boolean
|
|
76
|
-
setSectionBottom: (v: boolean) => void
|
|
77
|
-
/** Increment to force a balance re-fetch. */
|
|
78
|
-
balanceRefresh: () => number
|
|
79
|
-
setBalanceRefresh: (v: number) => void
|
|
80
|
-
/** Currently selected balance provider id (e.g. "deepseek"). */
|
|
81
|
-
balanceProviderId: () => string
|
|
82
|
-
setBalanceProviderId: (v: string) => void
|
|
83
|
-
/** Auto-switch to the session's provider for balance display. Manual switch disables it. */
|
|
84
|
-
autoBalance: () => boolean
|
|
85
|
-
setAutoBalance: (v: boolean) => void
|
|
86
|
-
/** True when the session's provider has no balance adapter (auto mode). Suppresses balance polling. */
|
|
87
|
-
balanceUnsupported: () => boolean
|
|
88
|
-
setBalanceUnsupported: (v: boolean) => void
|
|
89
|
-
/** Shared balance query state — single source of truth for sidebar and bottom bar. */
|
|
90
|
-
balanceState: () => BalanceState
|
|
91
|
-
/** Preferred currency code for balance display (CNY / USD / …). Empty = first entry. */
|
|
92
|
-
balanceCurrency: () => string
|
|
93
|
-
setBalanceCurrency: (v: string) => void
|
|
94
|
-
borderVisible: () => boolean
|
|
95
|
-
setBorderVisible: (v: boolean) => void
|
|
96
|
-
/** When set, the panel renders stats for this session instead of the main one. */
|
|
97
|
-
overrideSessionId: () => string | undefined
|
|
98
|
-
setOverrideSessionId: (v: string | undefined) => void
|
|
99
|
-
/** True while our sidebar panel is mounted — host sidebar is visible (occupies 42 cols). */
|
|
100
|
-
sidebarVisible: () => boolean
|
|
101
|
-
setSidebarVisible: (v: boolean) => void
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export type { TuiThemeCurrent }
|
|
1
|
+
import type { Message, Part, Session } from "@opencode-ai/sdk"
|
|
2
|
+
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
3
|
+
import type { BalanceEntry } from "../balance-providers"
|
|
4
|
+
import type { LangCode } from "../i18n"
|
|
5
|
+
|
|
6
|
+
/** 会话信息(面板消费的字段;SDK Session 类型过严,用宽松接口) */
|
|
7
|
+
export interface PanelSession {
|
|
8
|
+
id: string
|
|
9
|
+
title?: string
|
|
10
|
+
agent?: string
|
|
11
|
+
model?: { providerID?: string; id?: string }
|
|
12
|
+
tokens?: { input?: number; output?: number; reasoning?: number; cache?: { read?: number; write?: number } }
|
|
13
|
+
cost?: number
|
|
14
|
+
[key: string]: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Panel API 契约:TokenCachePanel 消费的宿主 API 子集。
|
|
19
|
+
* V1(TuiPluginApi)天然满足;V2(opencode2 context)由 v2-panel-api 适配实现。
|
|
20
|
+
*/
|
|
21
|
+
export interface PanelApi {
|
|
22
|
+
kv: {
|
|
23
|
+
ready: boolean
|
|
24
|
+
get<T>(key: string, fallback?: T): T | undefined
|
|
25
|
+
set(key: string, value: unknown): void | Promise<void>
|
|
26
|
+
}
|
|
27
|
+
state: {
|
|
28
|
+
session: {
|
|
29
|
+
get(id: string): PanelSession | undefined
|
|
30
|
+
/** V1 返回 sdk/v2 的 Message、V2 返回 SessionMessageInfo——统一放宽 */
|
|
31
|
+
messages(id: string): readonly any[]
|
|
32
|
+
}
|
|
33
|
+
provider: readonly Record<string, any>[]
|
|
34
|
+
config: any
|
|
35
|
+
part(messageID: string): readonly any[]
|
|
36
|
+
path: { directory: string }
|
|
37
|
+
}
|
|
38
|
+
event: {
|
|
39
|
+
on(type: string, handler: (event: unknown) => void): () => void
|
|
40
|
+
}
|
|
41
|
+
renderer: { terminalWidth: number }
|
|
42
|
+
keys: { formatBindings(binding: unknown): string | undefined }
|
|
43
|
+
tuiConfig: { keybinds: { get(command: string): unknown } }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BalanceState {
|
|
47
|
+
status: "idle" | "loading" | "ok" | "error"
|
|
48
|
+
data: BalanceEntry[] | null
|
|
49
|
+
lastFetch: number
|
|
50
|
+
error?: string
|
|
51
|
+
key?: string // 上次成功/尝试查询所用的 key,用于检测 key 是否更换
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Signals shared between the TUI component and slash commands.
|
|
55
|
+
* Created in the `tui` function scope so they do not survive module reload —
|
|
56
|
+
* the component re-creates them on mount and restores user config from kv. */
|
|
57
|
+
export interface PanelSignals {
|
|
58
|
+
currencySymbol: () => string
|
|
59
|
+
setCurrencySymbol: (v: string) => void
|
|
60
|
+
exchangeRate: () => number
|
|
61
|
+
setExchangeRate: (v: number) => void
|
|
62
|
+
langCode: () => LangCode
|
|
63
|
+
setLangCode: (v: LangCode) => void
|
|
64
|
+
sectionDetail: () => boolean
|
|
65
|
+
setSectionDetail: (v: boolean) => void
|
|
66
|
+
sectionModel: () => boolean
|
|
67
|
+
setSectionModel: (v: boolean) => void
|
|
68
|
+
sectionDist: () => boolean
|
|
69
|
+
setSectionDist: (v: boolean) => void
|
|
70
|
+
sectionSkills: () => boolean
|
|
71
|
+
setSectionSkills: (v: boolean) => void
|
|
72
|
+
sectionBalance: () => boolean
|
|
73
|
+
setSectionBalance: (v: boolean) => void
|
|
74
|
+
/** Bottom status bar (prompt hint line) visibility. */
|
|
75
|
+
sectionBottom: () => boolean
|
|
76
|
+
setSectionBottom: (v: boolean) => void
|
|
77
|
+
/** Increment to force a balance re-fetch. */
|
|
78
|
+
balanceRefresh: () => number
|
|
79
|
+
setBalanceRefresh: (v: number) => void
|
|
80
|
+
/** Currently selected balance provider id (e.g. "deepseek"). */
|
|
81
|
+
balanceProviderId: () => string
|
|
82
|
+
setBalanceProviderId: (v: string) => void
|
|
83
|
+
/** Auto-switch to the session's provider for balance display. Manual switch disables it. */
|
|
84
|
+
autoBalance: () => boolean
|
|
85
|
+
setAutoBalance: (v: boolean) => void
|
|
86
|
+
/** True when the session's provider has no balance adapter (auto mode). Suppresses balance polling. */
|
|
87
|
+
balanceUnsupported: () => boolean
|
|
88
|
+
setBalanceUnsupported: (v: boolean) => void
|
|
89
|
+
/** Shared balance query state — single source of truth for sidebar and bottom bar. */
|
|
90
|
+
balanceState: () => BalanceState
|
|
91
|
+
/** Preferred currency code for balance display (CNY / USD / …). Empty = first entry. */
|
|
92
|
+
balanceCurrency: () => string
|
|
93
|
+
setBalanceCurrency: (v: string) => void
|
|
94
|
+
borderVisible: () => boolean
|
|
95
|
+
setBorderVisible: (v: boolean) => void
|
|
96
|
+
/** When set, the panel renders stats for this session instead of the main one. */
|
|
97
|
+
overrideSessionId: () => string | undefined
|
|
98
|
+
setOverrideSessionId: (v: string | undefined) => void
|
|
99
|
+
/** True while our sidebar panel is mounted — host sidebar is visible (occupies 42 cols). */
|
|
100
|
+
sidebarVisible: () => boolean
|
|
101
|
+
setSidebarVisible: (v: boolean) => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type { TuiThemeCurrent }
|
package/src/v2/data.ts
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
import type { Context, MessageInfo } from "./types"
|
|
2
|
-
import { num } from "../core"
|
|
3
|
-
|
|
4
|
-
/** 汇总一个会话的 token 用量与费用(V2 adapter → 同构模型)。 */
|
|
5
|
-
export interface SessionStats {
|
|
6
|
-
input: number
|
|
7
|
-
output: number
|
|
8
|
-
cacheRead: number
|
|
9
|
-
cacheWrite: number
|
|
10
|
-
reasoning: number
|
|
11
|
-
cost: number
|
|
12
|
-
hasData: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** 从 messages 汇总 token:统计全部 assistant 消息的 API 精确值。 */
|
|
16
|
-
export function collectSessionStats(context: Context, sessionID: string): SessionStats {
|
|
17
|
-
const msgs = context.data.session.message.list(sessionID) ?? []
|
|
18
|
-
let input = 0
|
|
19
|
-
let output = 0
|
|
20
|
-
let cacheRead = 0
|
|
21
|
-
let cacheWrite = 0
|
|
22
|
-
let reasoning = 0
|
|
23
|
-
let cost = 0
|
|
24
|
-
let hasData = false
|
|
25
|
-
|
|
26
|
-
for (const m of msgs) {
|
|
27
|
-
if (m.type !== "assistant") continue
|
|
28
|
-
const t = m.tokens
|
|
29
|
-
if (!t) continue
|
|
30
|
-
input += num(t.input)
|
|
31
|
-
output += num(t.output)
|
|
32
|
-
reasoning += num(t.reasoning)
|
|
33
|
-
cacheRead += num(t.cache?.read)
|
|
34
|
-
cacheWrite += num(t.cache?.write)
|
|
35
|
-
hasData = true
|
|
36
|
-
}
|
|
37
|
-
// 会话级费用优先(V2 提供 cost()),消息级 cost 兜底(结构未知,宽松读取)
|
|
38
|
-
const sessionCost = context.data.session.cost(sessionID)
|
|
39
|
-
if (Number.isFinite(sessionCost) && sessionCost > 0) cost = sessionCost
|
|
40
|
-
else {
|
|
41
|
-
for (const m of msgs) {
|
|
42
|
-
if (m.type !== "assistant" || m.cost == null) continue
|
|
43
|
-
const c = m.cost as { amount?: unknown; total?: unknown } | { amount?: unknown } | number
|
|
44
|
-
if (typeof c === "number" && Number.isFinite(c)) cost += c
|
|
45
|
-
else {
|
|
46
|
-
const amt = (c as { amount?: unknown }).amount
|
|
47
|
-
if (typeof amt === "number" && Number.isFinite(amt)) cost += amt
|
|
48
|
-
else if (typeof amt === "string") {
|
|
49
|
-
const n = parseFloat(amt)
|
|
50
|
-
if (Number.isFinite(n)) cost += n
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return { input, output, cacheRead, cacheWrite, reasoning, cost, hasData }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** 缓存命中率(0–100):缓存读 /(新鲜输入 + 缓存读 + 缓存写),与 V1 口径一致。 */
|
|
59
|
-
export function hitRate(stats: SessionStats): number {
|
|
60
|
-
const denom = stats.input + stats.cacheRead + stats.cacheWrite
|
|
61
|
-
if (denom <= 0) return 0
|
|
62
|
-
return (stats.cacheRead / denom) * 100
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** 输入侧总量(底部栏口径,不含输出)。 */
|
|
66
|
-
export function inputTotal(stats: SessionStats): number {
|
|
67
|
-
return stats.input + stats.cacheRead + stats.cacheWrite
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** 单条命中率(V1 底部栏口径):最后一条有 token 的 assistant 消息,
|
|
71
|
-
* 分母含缓存写 read/(input+read+write);prevHitRate 为上一条(趋势用)。 */
|
|
72
|
-
export function collectLastHitRate(context: Context, sessionID: string): {
|
|
73
|
-
hitRate: number
|
|
74
|
-
prevHitRate: number
|
|
75
|
-
input: number
|
|
76
|
-
read: number
|
|
77
|
-
write: number
|
|
78
|
-
} {
|
|
79
|
-
const msgs = context.data.session.message.list(sessionID) ?? []
|
|
80
|
-
const session = context.data.session.get(sessionID) as { tokens?: { input?: number; cache?: { read?: number; write?: number } } } | undefined
|
|
81
|
-
let input = num(session?.tokens?.input)
|
|
82
|
-
let read = num(session?.tokens?.cache?.read)
|
|
83
|
-
let write = num(session?.tokens?.cache?.write)
|
|
84
|
-
// 无 session 聚合字段 → 遍历消息累加(与 V1 fallback 一致)
|
|
85
|
-
if (session?.tokens == null) {
|
|
86
|
-
for (const m of msgs) {
|
|
87
|
-
if (m.type !== "assistant") continue
|
|
88
|
-
const t = m.tokens
|
|
89
|
-
if (!t) continue
|
|
90
|
-
input += num(t.input)
|
|
91
|
-
read += num(t.cache?.read)
|
|
92
|
-
write += num(t.cache?.write)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
let hitRate = -1, prevHitRate = -1
|
|
96
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
97
|
-
const m = msgs[i]
|
|
98
|
-
if (m.type !== "assistant") continue
|
|
99
|
-
const tk = m.tokens
|
|
100
|
-
if (!tk) continue
|
|
101
|
-
const mit = num(tk.input) + num(tk.cache?.read) + num(tk.cache?.write)
|
|
102
|
-
const mrt = num(tk.cache?.read)
|
|
103
|
-
if (mit <= 0) continue
|
|
104
|
-
const rate = (mrt / mit) * 100
|
|
105
|
-
if (hitRate < 0) { hitRate = rate; continue }
|
|
106
|
-
prevHitRate = rate
|
|
107
|
-
break
|
|
108
|
-
}
|
|
109
|
-
return { hitRate, prevHitRate, input, read, write }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export type { MessageInfo }
|
|
1
|
+
import type { Context, MessageInfo } from "./types"
|
|
2
|
+
import { num } from "../core"
|
|
3
|
+
|
|
4
|
+
/** 汇总一个会话的 token 用量与费用(V2 adapter → 同构模型)。 */
|
|
5
|
+
export interface SessionStats {
|
|
6
|
+
input: number
|
|
7
|
+
output: number
|
|
8
|
+
cacheRead: number
|
|
9
|
+
cacheWrite: number
|
|
10
|
+
reasoning: number
|
|
11
|
+
cost: number
|
|
12
|
+
hasData: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 从 messages 汇总 token:统计全部 assistant 消息的 API 精确值。 */
|
|
16
|
+
export function collectSessionStats(context: Context, sessionID: string): SessionStats {
|
|
17
|
+
const msgs = context.data.session.message.list(sessionID) ?? []
|
|
18
|
+
let input = 0
|
|
19
|
+
let output = 0
|
|
20
|
+
let cacheRead = 0
|
|
21
|
+
let cacheWrite = 0
|
|
22
|
+
let reasoning = 0
|
|
23
|
+
let cost = 0
|
|
24
|
+
let hasData = false
|
|
25
|
+
|
|
26
|
+
for (const m of msgs) {
|
|
27
|
+
if (m.type !== "assistant") continue
|
|
28
|
+
const t = m.tokens
|
|
29
|
+
if (!t) continue
|
|
30
|
+
input += num(t.input)
|
|
31
|
+
output += num(t.output)
|
|
32
|
+
reasoning += num(t.reasoning)
|
|
33
|
+
cacheRead += num(t.cache?.read)
|
|
34
|
+
cacheWrite += num(t.cache?.write)
|
|
35
|
+
hasData = true
|
|
36
|
+
}
|
|
37
|
+
// 会话级费用优先(V2 提供 cost()),消息级 cost 兜底(结构未知,宽松读取)
|
|
38
|
+
const sessionCost = context.data.session.cost(sessionID)
|
|
39
|
+
if (Number.isFinite(sessionCost) && sessionCost > 0) cost = sessionCost
|
|
40
|
+
else {
|
|
41
|
+
for (const m of msgs) {
|
|
42
|
+
if (m.type !== "assistant" || m.cost == null) continue
|
|
43
|
+
const c = m.cost as { amount?: unknown; total?: unknown } | { amount?: unknown } | number
|
|
44
|
+
if (typeof c === "number" && Number.isFinite(c)) cost += c
|
|
45
|
+
else {
|
|
46
|
+
const amt = (c as { amount?: unknown }).amount
|
|
47
|
+
if (typeof amt === "number" && Number.isFinite(amt)) cost += amt
|
|
48
|
+
else if (typeof amt === "string") {
|
|
49
|
+
const n = parseFloat(amt)
|
|
50
|
+
if (Number.isFinite(n)) cost += n
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { input, output, cacheRead, cacheWrite, reasoning, cost, hasData }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 缓存命中率(0–100):缓存读 /(新鲜输入 + 缓存读 + 缓存写),与 V1 口径一致。 */
|
|
59
|
+
export function hitRate(stats: SessionStats): number {
|
|
60
|
+
const denom = stats.input + stats.cacheRead + stats.cacheWrite
|
|
61
|
+
if (denom <= 0) return 0
|
|
62
|
+
return (stats.cacheRead / denom) * 100
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 输入侧总量(底部栏口径,不含输出)。 */
|
|
66
|
+
export function inputTotal(stats: SessionStats): number {
|
|
67
|
+
return stats.input + stats.cacheRead + stats.cacheWrite
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 单条命中率(V1 底部栏口径):最后一条有 token 的 assistant 消息,
|
|
71
|
+
* 分母含缓存写 read/(input+read+write);prevHitRate 为上一条(趋势用)。 */
|
|
72
|
+
export function collectLastHitRate(context: Context, sessionID: string): {
|
|
73
|
+
hitRate: number
|
|
74
|
+
prevHitRate: number
|
|
75
|
+
input: number
|
|
76
|
+
read: number
|
|
77
|
+
write: number
|
|
78
|
+
} {
|
|
79
|
+
const msgs = context.data.session.message.list(sessionID) ?? []
|
|
80
|
+
const session = context.data.session.get(sessionID) as { tokens?: { input?: number; cache?: { read?: number; write?: number } } } | undefined
|
|
81
|
+
let input = num(session?.tokens?.input)
|
|
82
|
+
let read = num(session?.tokens?.cache?.read)
|
|
83
|
+
let write = num(session?.tokens?.cache?.write)
|
|
84
|
+
// 无 session 聚合字段 → 遍历消息累加(与 V1 fallback 一致)
|
|
85
|
+
if (session?.tokens == null) {
|
|
86
|
+
for (const m of msgs) {
|
|
87
|
+
if (m.type !== "assistant") continue
|
|
88
|
+
const t = m.tokens
|
|
89
|
+
if (!t) continue
|
|
90
|
+
input += num(t.input)
|
|
91
|
+
read += num(t.cache?.read)
|
|
92
|
+
write += num(t.cache?.write)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let hitRate = -1, prevHitRate = -1
|
|
96
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
97
|
+
const m = msgs[i]
|
|
98
|
+
if (m.type !== "assistant") continue
|
|
99
|
+
const tk = m.tokens
|
|
100
|
+
if (!tk) continue
|
|
101
|
+
const mit = num(tk.input) + num(tk.cache?.read) + num(tk.cache?.write)
|
|
102
|
+
const mrt = num(tk.cache?.read)
|
|
103
|
+
if (mit <= 0) continue
|
|
104
|
+
const rate = (mrt / mit) * 100
|
|
105
|
+
if (hitRate < 0) { hitRate = rate; continue }
|
|
106
|
+
prevHitRate = rate
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
return { hitRate, prevHitRate, input, read, write }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type { MessageInfo }
|