opencode-tweaks 0.5.0 → 0.6.1
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/README.md +8 -0
- package/package.json +1 -1
- package/src/tweaks/cost.ts +8 -0
- package/src/tweaks/usage-slots.tsx +25 -3
- package/src/usage/messages.ts +2 -0
- package/src/usage/price-window.ts +157 -0
package/README.md
CHANGED
|
@@ -179,6 +179,13 @@ Costs come from what the provider reports, so a free plan shows `$0.00`. `Cost d
|
|
|
179
179
|
`Auto` keeps two decimals and grows until the first significant digit shows (`$0.004`), while a fixed choice always
|
|
180
180
|
keeps that many (`$0.0042`).
|
|
181
181
|
|
|
182
|
+
`Price windows` marks rounds billed above the catalog price. DeepSeek doubles its listed prices from 01:00 to
|
|
183
|
+
04:00 and from 06:00 to 10:00 UTC, Monday to Friday, and bills Chinese public holidays at the off-peak rate; OpenCode
|
|
184
|
+
Go publishes the same DeepSeek rates. The badge reads `peak ×2` beside the turn cost when that round started in a peak
|
|
185
|
+
window, so it stays on screen after the window closes. The catalog already holds the off-peak price, so the figures
|
|
186
|
+
themselves stay the listed rate. The 2026 and 2027 holidays are built in; update `src/usage/price-window.ts` when the
|
|
187
|
+
next calendar is published.
|
|
188
|
+
|
|
182
189
|
`Estimate cost` fills in a price the provider did not report, using the model's list price from the same catalog
|
|
183
190
|
OpenCode already synced: prompt, completion, reasoning billed as output, and cache traffic, each at its own rate. A
|
|
184
191
|
provider that reports a price always wins, and a session that changed model is estimated with the model it ended on, so
|
|
@@ -212,6 +219,7 @@ src/
|
|
|
212
219
|
messages.ts message shape and turn selection
|
|
213
220
|
tokens.ts token sums and formatting
|
|
214
221
|
cost.ts list prices, estimates, and cost formatting
|
|
222
|
+
price-window.ts provider peak hours and holidays
|
|
215
223
|
tui.tsx terminal entry: commands and slots
|
|
216
224
|
tweaks/
|
|
217
225
|
types.ts settings schema and validation
|
package/package.json
CHANGED
package/src/tweaks/cost.ts
CHANGED
|
@@ -57,6 +57,14 @@ export const costTweak = defineTweak({
|
|
|
57
57
|
values: ["auto", 2, 3, 4, 5, 6],
|
|
58
58
|
labels: ["Auto", "2 digits", "3 digits", "4 digits", "5 digits", "6 digits"],
|
|
59
59
|
},
|
|
60
|
+
{
|
|
61
|
+
id: "windows",
|
|
62
|
+
title: "Price windows",
|
|
63
|
+
description: "Mark rounds billed above the catalog price, such as DeepSeek peak hours.",
|
|
64
|
+
category: "Display",
|
|
65
|
+
kind: "boolean",
|
|
66
|
+
default: true,
|
|
67
|
+
},
|
|
60
68
|
{
|
|
61
69
|
id: "estimate",
|
|
62
70
|
title: "Estimate cost",
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
priceFor,
|
|
11
11
|
} from "../usage/cost"
|
|
12
12
|
import { footerMessage, replyTurnMessages, type UsageMessage } from "../usage/messages"
|
|
13
|
+
import { formatPriceWindow, type PriceModel, turnPriceWindow } from "../usage/price-window"
|
|
13
14
|
import { formatTokens, sessionTokens, type TokenCount, usageTokens } from "../usage/tokens"
|
|
14
15
|
import { costTweak } from "./cost"
|
|
15
16
|
import type { SettingsStore } from "./settings"
|
|
@@ -23,6 +24,15 @@ function setting(tweak: Tweak, id: string): TweakSetting {
|
|
|
23
24
|
return found
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/** The model of the newest message that carries one. */
|
|
28
|
+
function lastModel(messages: readonly UsageMessage[]): PriceModel | undefined {
|
|
29
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
30
|
+
const model = messages[index]?.model
|
|
31
|
+
if (model) return model
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
const TOKENS_ON = setting(tokensTweak, "enabled")
|
|
27
37
|
const PROMPT = setting(tokensTweak, "prompt")
|
|
28
38
|
const TOTAL = setting(tokensTweak, "total")
|
|
@@ -35,6 +45,7 @@ const COST_ON = setting(costTweak, "enabled")
|
|
|
35
45
|
const PROMPT_COST = setting(costTweak, "prompt")
|
|
36
46
|
const TOTAL_COST = setting(costTweak, "total")
|
|
37
47
|
const COST_DIGITS = setting(costTweak, "digits")
|
|
48
|
+
const WINDOWS = setting(costTweak, "windows")
|
|
38
49
|
const ESTIMATE = setting(costTweak, "estimate")
|
|
39
50
|
|
|
40
51
|
/**
|
|
@@ -55,6 +66,7 @@ function useUsage(props: {
|
|
|
55
66
|
const subagents = () => props.store.read(tokensTweak, SUBAGENTS).value === true
|
|
56
67
|
const promptCost = () => props.store.read(costTweak, PROMPT_COST).value === true
|
|
57
68
|
const totalCost = () => props.store.read(costTweak, TOTAL_COST).value === true
|
|
69
|
+
const windowsOn = () => props.store.read(costTweak, WINDOWS).value === true
|
|
58
70
|
const estimate = () => props.store.read(costTweak, ESTIMATE).value === true
|
|
59
71
|
const ttft = () => props.store.read(tokensTweak, TTFT).value !== false
|
|
60
72
|
const costDigits = (): CostDigits => {
|
|
@@ -173,12 +185,22 @@ function useUsage(props: {
|
|
|
173
185
|
|
|
174
186
|
const promptCount = () => (tokensOn() && perPrompt() ? format(prompt()) : undefined)
|
|
175
187
|
const totalCount = () => (tokensOn() && sessionTotal() ? format(total()) : undefined)
|
|
188
|
+
|
|
189
|
+
const sessionModel = () => {
|
|
190
|
+
const sessionID = props.sessionID()
|
|
191
|
+
if (!sessionID) return undefined
|
|
192
|
+
return sessions.get(sessionID)?.model ?? lastModel(messages(sessionID))
|
|
193
|
+
}
|
|
194
|
+
|
|
176
195
|
const promptCostLabel = () => {
|
|
177
196
|
const value = cost()
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
197
|
+
if (!costOn() || !promptCost() || value === undefined) return undefined
|
|
198
|
+
|
|
199
|
+
const label = formatCost(value, costDigits())
|
|
200
|
+
const window = windowsOn() ? turnPriceWindow(turn(), sessionModel()) : undefined
|
|
201
|
+
return window ? `${label} · ${formatPriceWindow(window)}` : label
|
|
181
202
|
}
|
|
203
|
+
|
|
182
204
|
const totalCostLabel = () =>
|
|
183
205
|
costOn() && totalCost() ? formatCost(sessionCost(), costDigits()) : undefined
|
|
184
206
|
|
package/src/usage/messages.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface UsageMessage {
|
|
|
13
13
|
readonly tokens?: MessageTokens
|
|
14
14
|
readonly cost?: number
|
|
15
15
|
readonly model?: { readonly providerID: string; readonly id: string }
|
|
16
|
+
/** When the step was created, which is when the provider started billing it. */
|
|
17
|
+
readonly time?: { readonly created?: number }
|
|
16
18
|
/** How the step ended; the host only draws a footer for a finished one. */
|
|
17
19
|
readonly finish?: string | null
|
|
18
20
|
readonly error?: unknown
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { UsageMessage } from "./messages"
|
|
2
|
+
|
|
3
|
+
/** The provider and model a message belongs to. */
|
|
4
|
+
export interface PriceModel {
|
|
5
|
+
readonly providerID: string
|
|
6
|
+
readonly id: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A period that bills above the catalog price. */
|
|
10
|
+
export interface PriceWindow {
|
|
11
|
+
/** Short label shown beside the cost, such as `peak`. */
|
|
12
|
+
readonly label: string
|
|
13
|
+
/** What the period does to the catalog price, such as 2 for double. */
|
|
14
|
+
readonly multiplier: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PeakRange {
|
|
18
|
+
/** Minutes after midnight UTC, inclusive. */
|
|
19
|
+
readonly from: number
|
|
20
|
+
/** Minutes after midnight UTC, exclusive. */
|
|
21
|
+
readonly to: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PriceSchedule {
|
|
25
|
+
/** Providers whose catalog price follows this schedule. */
|
|
26
|
+
readonly providers: readonly string[]
|
|
27
|
+
/** Model id prefix the schedule covers, when the provider serves other models. */
|
|
28
|
+
readonly modelPrefix: string
|
|
29
|
+
/** Weekdays billed at the higher price, where 1 is Monday and 7 is Sunday. */
|
|
30
|
+
readonly peakDays: readonly number[]
|
|
31
|
+
/** Peak windows in UTC; none of them crosses midnight. */
|
|
32
|
+
readonly peak: readonly PeakRange[]
|
|
33
|
+
/** Days billed at the catalog price even when they fall on a peak weekday. */
|
|
34
|
+
readonly holidays?: ReadonlySet<string>
|
|
35
|
+
readonly label: string
|
|
36
|
+
readonly multiplier: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Every UTC date from `from` to `to`, both included. */
|
|
40
|
+
function dateRange(from: string, to: string): string[] {
|
|
41
|
+
const dates: string[] = []
|
|
42
|
+
const end = Date.parse(`${to}T00:00:00Z`)
|
|
43
|
+
|
|
44
|
+
for (let at = Date.parse(`${from}T00:00:00Z`); at <= end; at += 86_400_000) {
|
|
45
|
+
dates.push(new Date(at).toISOString().slice(0, 10))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return dates
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Mainland China public holidays from the State Council notices of 2025-11-04
|
|
53
|
+
* (2026) and 2026-11-05 (2027). DeepSeek bills these days at the off-peak rate
|
|
54
|
+
* even when they fall on a weekday. Add the next notice's dates here.
|
|
55
|
+
*/
|
|
56
|
+
const CHINA_HOLIDAYS: ReadonlySet<string> = new Set([
|
|
57
|
+
...dateRange("2026-01-01", "2026-01-03"),
|
|
58
|
+
...dateRange("2026-02-15", "2026-02-23"),
|
|
59
|
+
...dateRange("2026-04-04", "2026-04-06"),
|
|
60
|
+
...dateRange("2026-05-01", "2026-05-05"),
|
|
61
|
+
...dateRange("2026-06-19", "2026-06-21"),
|
|
62
|
+
...dateRange("2026-09-25", "2026-09-27"),
|
|
63
|
+
...dateRange("2026-10-01", "2026-10-07"),
|
|
64
|
+
...dateRange("2027-01-01", "2027-01-03"),
|
|
65
|
+
...dateRange("2027-02-05", "2027-02-13"),
|
|
66
|
+
...dateRange("2027-04-03", "2027-04-05"),
|
|
67
|
+
...dateRange("2027-05-01", "2027-05-05"),
|
|
68
|
+
...dateRange("2027-06-09", "2027-06-09"),
|
|
69
|
+
...dateRange("2027-09-15", "2027-09-15"),
|
|
70
|
+
...dateRange("2027-10-01", "2027-10-07"),
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* DeepSeek doubles its catalog price from 01:00 to 04:00 and from 06:00 to
|
|
75
|
+
* 10:00 UTC, Monday to Friday, excluding Chinese public holidays. Its catalog
|
|
76
|
+
* prices are the off-peak rates, so the higher price is twice the listed one.
|
|
77
|
+
* OpenCode Go publishes the same DeepSeek rates.
|
|
78
|
+
*
|
|
79
|
+
* Source: https://api-docs.deepseek.com/quick_start/pricing/ (2026-11)
|
|
80
|
+
*/
|
|
81
|
+
const DEEPSEEK: PriceSchedule = {
|
|
82
|
+
providers: ["deepseek", "opencode-go"],
|
|
83
|
+
modelPrefix: "deepseek-",
|
|
84
|
+
peakDays: [1, 2, 3, 4, 5],
|
|
85
|
+
peak: [
|
|
86
|
+
{ from: 1 * 60, to: 4 * 60 },
|
|
87
|
+
{ from: 6 * 60, to: 10 * 60 },
|
|
88
|
+
],
|
|
89
|
+
holidays: CHINA_HOLIDAYS,
|
|
90
|
+
label: "peak",
|
|
91
|
+
multiplier: 2,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const schedules: readonly PriceSchedule[] = [DEEPSEEK]
|
|
95
|
+
|
|
96
|
+
/** Monday-first weekday, since the schedules are written that way. */
|
|
97
|
+
function weekday(date: Date): number {
|
|
98
|
+
const day = date.getUTCDay()
|
|
99
|
+
return day === 0 ? 7 : day
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function utcDate(date: Date): string {
|
|
103
|
+
return date.toISOString().slice(0, 10)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function utcMinutes(date: Date): number {
|
|
107
|
+
return date.getUTCHours() * 60 + date.getUTCMinutes()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The higher price that applies to a model at `at`, or `undefined` when the
|
|
112
|
+
* catalog price is the one being charged. The check is deliberately narrow:
|
|
113
|
+
* only published schedules show a badge, and an unknown provider stays silent.
|
|
114
|
+
*/
|
|
115
|
+
export function priceWindowFor(model: PriceModel | undefined, at: Date): PriceWindow | undefined {
|
|
116
|
+
if (!model) return undefined
|
|
117
|
+
|
|
118
|
+
const schedule = schedules.find(
|
|
119
|
+
(candidate) =>
|
|
120
|
+
candidate.providers.includes(model.providerID) && model.id.startsWith(candidate.modelPrefix),
|
|
121
|
+
)
|
|
122
|
+
if (!schedule) return undefined
|
|
123
|
+
|
|
124
|
+
if (!schedule.peakDays.includes(weekday(at))) return undefined
|
|
125
|
+
if (schedule.holidays?.has(utcDate(at))) return undefined
|
|
126
|
+
|
|
127
|
+
const minutes = utcMinutes(at)
|
|
128
|
+
if (!schedule.peak.some((range) => minutes >= range.from && minutes < range.to)) {
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { label: schedule.label, multiplier: schedule.multiplier }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The short text shown beside a cost, such as `peak ×2`. */
|
|
136
|
+
export function formatPriceWindow(window: PriceWindow): string {
|
|
137
|
+
return `${window.label} ×${window.multiplier}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The window that applied to a round, from the step that started it. Each
|
|
142
|
+
* request is billed when it starts, so a round that ran during peak keeps its
|
|
143
|
+
* marker after the window closes. Only assistant steps carry a start time.
|
|
144
|
+
*/
|
|
145
|
+
export function turnPriceWindow(
|
|
146
|
+
messages: readonly UsageMessage[],
|
|
147
|
+
fallback?: PriceModel,
|
|
148
|
+
): PriceWindow | undefined {
|
|
149
|
+
const step = messages.find(
|
|
150
|
+
(message) => message.type === "assistant" && message.time?.created !== undefined,
|
|
151
|
+
)
|
|
152
|
+
const created = step?.time?.created
|
|
153
|
+
const model = step?.model ?? fallback
|
|
154
|
+
if (!model || created === undefined) return undefined
|
|
155
|
+
|
|
156
|
+
return priceWindowFor(model, new Date(created))
|
|
157
|
+
}
|