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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-tweaks",
4
- "version": "0.5.0",
4
+ "version": "0.6.1",
5
5
  "description": "Opt-in settings and small interface improvements for OpenCode.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -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
- return costOn() && promptCost() && value !== undefined
179
- ? formatCost(value, costDigits())
180
- : undefined
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
 
@@ -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
+ }