opencode-tweaks 0.6.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 +5 -4
- package/package.json +1 -1
- package/src/tweaks/cost.ts +1 -2
- package/src/tweaks/usage-slots.tsx +4 -27
- package/src/usage/messages.ts +2 -0
- package/src/usage/price-window.ts +21 -0
package/README.md
CHANGED
|
@@ -179,11 +179,12 @@ 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
|
|
182
|
+
`Price windows` marks rounds billed above the catalog price. DeepSeek doubles its listed prices from 01:00 to
|
|
183
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`
|
|
185
|
-
|
|
186
|
-
`src/usage/price-window.ts` when the
|
|
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.
|
|
187
188
|
|
|
188
189
|
`Estimate cost` fills in a price the provider did not report, using the model's list price from the same catalog
|
|
189
190
|
OpenCode already synced: prompt, completion, reasoning billed as output, and cache traffic, each at its own rate. A
|
package/package.json
CHANGED
package/src/tweaks/cost.ts
CHANGED
|
@@ -60,8 +60,7 @@ export const costTweak = defineTweak({
|
|
|
60
60
|
{
|
|
61
61
|
id: "windows",
|
|
62
62
|
title: "Price windows",
|
|
63
|
-
description:
|
|
64
|
-
"Mark when the provider charges more than the catalog price, such as DeepSeek peak hours.",
|
|
63
|
+
description: "Mark rounds billed above the catalog price, such as DeepSeek peak hours.",
|
|
65
64
|
category: "Display",
|
|
66
65
|
kind: "boolean",
|
|
67
66
|
default: true,
|
|
@@ -10,7 +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,
|
|
13
|
+
import { formatPriceWindow, type PriceModel, turnPriceWindow } from "../usage/price-window"
|
|
14
14
|
import { formatTokens, sessionTokens, type TokenCount, usageTokens } from "../usage/tokens"
|
|
15
15
|
import { costTweak } from "./cost"
|
|
16
16
|
import type { SettingsStore } from "./settings"
|
|
@@ -84,19 +84,6 @@ function useUsage(props: {
|
|
|
84
84
|
|
|
85
85
|
const sessions = props.context.data.session
|
|
86
86
|
|
|
87
|
-
const [now, setNow] = createSignal(Date.now())
|
|
88
|
-
|
|
89
|
-
// The badge follows the clock, not the transcript: a line that stays on
|
|
90
|
-
// screen across a rate change has to update on its own. The timer exists
|
|
91
|
-
// only while the cost tweak and its price windows are on.
|
|
92
|
-
createEffect(() => {
|
|
93
|
-
if (!costOn() || !windowsOn() || (!promptCost() && !totalCost())) return
|
|
94
|
-
|
|
95
|
-
setNow(Date.now())
|
|
96
|
-
const timer = setInterval(() => setNow(Date.now()), 30_000)
|
|
97
|
-
onCleanup(() => clearInterval(timer))
|
|
98
|
-
})
|
|
99
|
-
|
|
100
87
|
// List prices come from the model catalog the TUI already synced, so an
|
|
101
88
|
// estimate costs nothing but a lookup.
|
|
102
89
|
const prices = createMemo(() => {
|
|
@@ -205,27 +192,17 @@ function useUsage(props: {
|
|
|
205
192
|
return sessions.get(sessionID)?.model ?? lastModel(messages(sessionID))
|
|
206
193
|
}
|
|
207
194
|
|
|
208
|
-
/** The higher price that applies right now, when the cost figures are on. */
|
|
209
|
-
const windowFor = (model: PriceModel | undefined) =>
|
|
210
|
-
costOn() && windowsOn() ? priceWindowFor(model, new Date(now())) : undefined
|
|
211
|
-
|
|
212
195
|
const promptCostLabel = () => {
|
|
213
196
|
const value = cost()
|
|
214
197
|
if (!costOn() || !promptCost() || value === undefined) return undefined
|
|
215
198
|
|
|
216
199
|
const label = formatCost(value, costDigits())
|
|
217
|
-
const window =
|
|
200
|
+
const window = windowsOn() ? turnPriceWindow(turn(), sessionModel()) : undefined
|
|
218
201
|
return window ? `${label} · ${formatPriceWindow(window)}` : label
|
|
219
202
|
}
|
|
220
203
|
|
|
221
|
-
const totalCostLabel = () =>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const value = sessionCost()
|
|
225
|
-
const label = formatCost(value, costDigits())
|
|
226
|
-
const window = value > 0 ? windowFor(sessionModel()) : undefined
|
|
227
|
-
return window ? `${label} · ${formatPriceWindow(window)}` : label
|
|
228
|
-
}
|
|
204
|
+
const totalCostLabel = () =>
|
|
205
|
+
costOn() && totalCost() ? formatCost(sessionCost(), costDigits()) : undefined
|
|
229
206
|
|
|
230
207
|
return {
|
|
231
208
|
enabled: () => tokensOn() || costOn(),
|
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
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { UsageMessage } from "./messages"
|
|
2
|
+
|
|
1
3
|
/** The provider and model a message belongs to. */
|
|
2
4
|
export interface PriceModel {
|
|
3
5
|
readonly providerID: string
|
|
@@ -134,3 +136,22 @@ export function priceWindowFor(model: PriceModel | undefined, at: Date): PriceWi
|
|
|
134
136
|
export function formatPriceWindow(window: PriceWindow): string {
|
|
135
137
|
return `${window.label} ×${window.multiplier}`
|
|
136
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
|
+
}
|