opencode-tweaks 0.7.1 → 0.8.0
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 +13 -9
- package/package.json +1 -1
- package/src/tweaks/model-panel.tsx +58 -70
- package/src/tweaks/model.ts +16 -7
package/README.md
CHANGED
|
@@ -197,17 +197,21 @@ The cost settings used to live in the Tokens entry. A choice saved there before
|
|
|
197
197
|
|
|
198
198
|
### Model
|
|
199
199
|
|
|
200
|
-
Model shows the model
|
|
201
|
-
|
|
202
|
-
expand
|
|
200
|
+
Model shows the current model's list price in the sidebar, under the quota. The model name, provider, and variant
|
|
201
|
+
already sit on the bottom bar, so the card only carries the rates and the peak state. Click the `Model` header to
|
|
202
|
+
collapse or expand it.
|
|
203
203
|
|
|
204
204
|
`Input price`, `Output price`, and `Cache read price` choose the rates, all in dollars per million tokens. The card
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
draws them as a price list with the labels on the left and the values in the theme accent. `Price alignment` picks
|
|
206
|
+
whether the prices start on one shared column (`left`) or sit against the right edge (`right`). It uses the catalog
|
|
207
|
+
price that applies to the session's context, so a long-context tier shows the higher rate with its threshold
|
|
208
|
+
(`>512K`).
|
|
209
|
+
|
|
210
|
+
`Peak state` puts the price multiplier on the right of the card header for schedules the plugin knows, such as
|
|
211
|
+
DeepSeek and OpenCode Go: `×2` in the warning colour during the higher window and `×1` in the normal text colour off
|
|
212
|
+
it. A model without a schedule shows nothing. The marker stays visible when the card is collapsed. `Peak only` hides
|
|
213
|
+
`×1`, so the header stays empty at the base price. The badge on the turn cost still records the window each round
|
|
214
|
+
started in.
|
|
211
215
|
|
|
212
216
|
## Development
|
|
213
217
|
|
package/package.json
CHANGED
|
@@ -18,19 +18,20 @@ const ENABLED = setting("enabled")
|
|
|
18
18
|
const INPUT = setting("input")
|
|
19
19
|
const OUTPUT = setting("output")
|
|
20
20
|
const CACHE = setting("cache")
|
|
21
|
-
const VARIANT = setting("variant")
|
|
22
21
|
const PEAK = setting("peak")
|
|
22
|
+
const PEAK_ONLY = setting("peakOnly")
|
|
23
|
+
const ALIGN = setting("align")
|
|
23
24
|
|
|
24
25
|
/** The ref stored on a session or returned by the composer. */
|
|
25
26
|
interface ModelRef {
|
|
26
27
|
readonly providerID: string
|
|
27
28
|
readonly id: string
|
|
28
|
-
readonly variant?: string
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* The model card
|
|
33
|
-
*
|
|
32
|
+
* The model card sits under the quota and carries what the bottom bar does
|
|
33
|
+
* not: the current model's list price and its provider's peak state. The
|
|
34
|
+
* model name and variant already live on that bar, so the card stays compact.
|
|
34
35
|
*/
|
|
35
36
|
export function ModelPanel(props: {
|
|
36
37
|
readonly context: Context
|
|
@@ -58,8 +59,9 @@ function ModelBody(props: {
|
|
|
58
59
|
const inputOn = () => props.store.read(modelTweak, INPUT).value === true
|
|
59
60
|
const outputOn = () => props.store.read(modelTweak, OUTPUT).value === true
|
|
60
61
|
const cacheOn = () => props.store.read(modelTweak, CACHE).value === true
|
|
61
|
-
const variantOn = () => props.store.read(modelTweak, VARIANT).value === true
|
|
62
62
|
const peakOn = () => props.store.read(modelTweak, PEAK).value === true
|
|
63
|
+
const peakOnly = () => props.store.read(modelTweak, PEAK_ONLY).value === true
|
|
64
|
+
const alignRight = () => props.store.read(modelTweak, ALIGN).value === "right"
|
|
63
65
|
|
|
64
66
|
/** The session's model, or the composer's while the session has none yet. */
|
|
65
67
|
const model = createMemo<ModelRef | undefined>(() => {
|
|
@@ -67,31 +69,21 @@ function ModelBody(props: {
|
|
|
67
69
|
if (session?.model) return session.model
|
|
68
70
|
|
|
69
71
|
const current = props.context.ui.model.current()
|
|
70
|
-
return current
|
|
71
|
-
? { providerID: current.providerID, id: current.modelID, variant: current.variant }
|
|
72
|
-
: undefined
|
|
72
|
+
return current ? { providerID: current.providerID, id: current.modelID } : undefined
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
-
/** The catalog
|
|
76
|
-
const
|
|
75
|
+
/** The catalog rates for that model, when the catalog knows it. */
|
|
76
|
+
const costs = createMemo(() => {
|
|
77
77
|
const ref = model()
|
|
78
|
-
if (!ref) return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
(
|
|
82
|
-
candidate
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
(candidate) => candidate.id === ref.providerID,
|
|
78
|
+
if (!ref) return []
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
(props.context.data.location.model.list() ?? []).find(
|
|
82
|
+
(candidate) =>
|
|
83
|
+
candidate.providerID === ref.providerID &&
|
|
84
|
+
(candidate.modelID === ref.id || candidate.id === ref.id),
|
|
85
|
+
)?.cost ?? []
|
|
87
86
|
)
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
ref,
|
|
91
|
-
name: found?.name ?? ref.id,
|
|
92
|
-
provider: provider?.name,
|
|
93
|
-
costs: found?.cost ?? [],
|
|
94
|
-
}
|
|
95
87
|
})
|
|
96
88
|
|
|
97
89
|
/** The context the session has read so far, which picks a long-context tier. */
|
|
@@ -101,52 +93,52 @@ function ModelBody(props: {
|
|
|
101
93
|
})
|
|
102
94
|
|
|
103
95
|
const price = createMemo(() => {
|
|
104
|
-
const
|
|
105
|
-
return
|
|
96
|
+
const list = costs()
|
|
97
|
+
return list.length > 0 ? priceFor(list, contextSize()) : undefined
|
|
106
98
|
})
|
|
107
99
|
|
|
108
100
|
const rates = createMemo(() => {
|
|
109
101
|
const value = price()
|
|
110
102
|
if (!value) return []
|
|
111
103
|
|
|
112
|
-
const rows: { label: string; value: string }[] = []
|
|
113
|
-
if (inputOn()) rows.push({ label: "
|
|
114
|
-
if (outputOn()) rows.push({ label: "
|
|
115
|
-
if (cacheOn()) rows.push({ label: "
|
|
104
|
+
const rows: { label: string; value: string; muted?: boolean }[] = []
|
|
105
|
+
if (inputOn()) rows.push({ label: "Input", value: formatRate(value.input) })
|
|
106
|
+
if (outputOn()) rows.push({ label: "Output", value: formatRate(value.output) })
|
|
107
|
+
if (cacheOn()) rows.push({ label: "Cache", value: formatRate(value.cache.read) })
|
|
116
108
|
if (rows.length > 0 && value.tier) {
|
|
117
|
-
rows.push({
|
|
109
|
+
rows.push({
|
|
110
|
+
label: "From",
|
|
111
|
+
value: `>${formatTokens(value.tier.size, "compact")}`,
|
|
112
|
+
muted: true,
|
|
113
|
+
})
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
return rows
|
|
121
117
|
})
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const selected = variant()
|
|
128
|
-
return selected ? `${name} · ${selected}` : name
|
|
129
|
-
}
|
|
119
|
+
/** The widest label, so the prices start on one column. */
|
|
120
|
+
const labelWidth = createMemo(() =>
|
|
121
|
+
rates().reduce((width, row) => Math.max(width, row.label.length), 0),
|
|
122
|
+
)
|
|
130
123
|
|
|
131
124
|
const scheduled = () => {
|
|
132
|
-
const ref =
|
|
125
|
+
const ref = model()
|
|
133
126
|
return ref ? hasPriceSchedule(ref) : false
|
|
134
127
|
}
|
|
135
128
|
|
|
136
129
|
const peak = () => {
|
|
137
|
-
const ref =
|
|
130
|
+
const ref = model()
|
|
138
131
|
if (!ref || !peakOn() || !scheduled()) return undefined
|
|
139
132
|
return priceWindowFor(ref, new Date(now()))
|
|
140
133
|
}
|
|
141
134
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
135
|
+
/** The price multiplier now, `1` when the catalog price is the one charged. */
|
|
136
|
+
const multiplier = () => peak()?.multiplier ?? 1
|
|
137
|
+
|
|
138
|
+
const showMultiplier = () => peakOn() && scheduled() && (peak() !== undefined || !peakOnly())
|
|
147
139
|
|
|
148
140
|
// The live peak marker follows the clock, so refresh it while the card can
|
|
149
|
-
// show one. Models without a schedule and a hidden peak
|
|
141
|
+
// show one. Models without a schedule and a hidden peak state need no timer.
|
|
150
142
|
createEffect(() => {
|
|
151
143
|
if (!peakOn() || !scheduled()) return
|
|
152
144
|
|
|
@@ -156,7 +148,7 @@ function ModelBody(props: {
|
|
|
156
148
|
})
|
|
157
149
|
|
|
158
150
|
return (
|
|
159
|
-
<box flexDirection="column"
|
|
151
|
+
<box flexDirection="column">
|
|
160
152
|
<box
|
|
161
153
|
flexDirection="row"
|
|
162
154
|
justifyContent="space-between"
|
|
@@ -165,37 +157,33 @@ function ModelBody(props: {
|
|
|
165
157
|
<text attributes={TextAttributes.BOLD} fg={theme().text.base}>
|
|
166
158
|
{collapsed() ? "▶" : "▼"} Model
|
|
167
159
|
</text>
|
|
168
|
-
<Show when={
|
|
169
|
-
<text
|
|
170
|
-
{
|
|
160
|
+
<Show when={showMultiplier()}>
|
|
161
|
+
<text
|
|
162
|
+
fg={peak() ? theme().text.feedback.warning.base : theme().text.base}
|
|
163
|
+
wrapMode="none"
|
|
164
|
+
>
|
|
165
|
+
{`×${multiplier()}`}
|
|
171
166
|
</text>
|
|
172
167
|
</Show>
|
|
173
168
|
</box>
|
|
174
169
|
<Show when={!collapsed()}>
|
|
175
|
-
<Show when={
|
|
176
|
-
<text fg={theme().text.base} wrapMode="word">
|
|
177
|
-
{title()}
|
|
178
|
-
</text>
|
|
170
|
+
<Show when={model()} fallback={<text fg={theme().text.muted}>No model selected</text>}>
|
|
179
171
|
<For each={rates()}>
|
|
180
172
|
{(row) => (
|
|
181
|
-
<box
|
|
182
|
-
|
|
183
|
-
|
|
173
|
+
<box
|
|
174
|
+
flexDirection="row"
|
|
175
|
+
justifyContent={alignRight() ? "space-between" : "flex-start"}
|
|
176
|
+
gap={1}
|
|
177
|
+
>
|
|
178
|
+
<text flexShrink={0} fg={theme().text.base}>
|
|
179
|
+
{alignRight() ? row.label : row.label.padEnd(labelWidth())}
|
|
180
|
+
</text>
|
|
181
|
+
<text flexShrink={0} fg={row.muted ? theme().text.muted : theme().hue.accent[200]}>
|
|
182
|
+
{row.value}
|
|
184
183
|
</text>
|
|
185
|
-
<text fg={theme().text.base}>{row.value}</text>
|
|
186
184
|
</box>
|
|
187
185
|
)}
|
|
188
186
|
</For>
|
|
189
|
-
<Show when={stateValue()}>
|
|
190
|
-
<box flexDirection="row" gap={1}>
|
|
191
|
-
<text flexShrink={0} fg={theme().text.muted}>
|
|
192
|
-
{"state".padEnd(5)}
|
|
193
|
-
</text>
|
|
194
|
-
<text fg={peak() ? theme().text.feedback.warning.base : theme().text.muted}>
|
|
195
|
-
{stateValue()}
|
|
196
|
-
</text>
|
|
197
|
-
</box>
|
|
198
|
-
</Show>
|
|
199
187
|
</Show>
|
|
200
188
|
</Show>
|
|
201
189
|
</box>
|
package/src/tweaks/model.ts
CHANGED
|
@@ -44,20 +44,29 @@ export const modelTweak = defineTweak({
|
|
|
44
44
|
default: true,
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
|
-
id: "
|
|
48
|
-
title: "
|
|
49
|
-
description: "Show the
|
|
47
|
+
id: "peak",
|
|
48
|
+
title: "Peak state",
|
|
49
|
+
description: "Show the model's current price multiplier in the card header.",
|
|
50
50
|
category: "Display",
|
|
51
51
|
kind: "boolean",
|
|
52
52
|
default: true,
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
|
-
id: "
|
|
56
|
-
title: "
|
|
57
|
-
description: "
|
|
55
|
+
id: "align",
|
|
56
|
+
title: "Price alignment",
|
|
57
|
+
description: "Start the prices on one column, or push them against the right edge.",
|
|
58
|
+
category: "Display",
|
|
59
|
+
kind: "select",
|
|
60
|
+
default: "right",
|
|
61
|
+
values: ["left", "right"],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "peakOnly",
|
|
65
|
+
title: "Peak only",
|
|
66
|
+
description: "Show the multiplier only during a peak window.",
|
|
58
67
|
category: "Display",
|
|
59
68
|
kind: "boolean",
|
|
60
|
-
default:
|
|
69
|
+
default: false,
|
|
61
70
|
},
|
|
62
71
|
],
|
|
63
72
|
})
|