@quadiusai/dsh-greenharness 0.1.0 → 0.1.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/GETTING-STARTED.md +26 -12
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/control.mjs +128 -27
- package/src/index.mjs +4 -3
package/GETTING-STARTED.md
CHANGED
|
@@ -14,22 +14,33 @@ Then start `dsh web` as usual. That's it. GreenHarness is now on.
|
|
|
14
14
|
|
|
15
15
|
## 2. Add your API keys and pick your models (no files to edit)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
1. In DSH web, open **Settings > Models**. Add your provider (for example NVIDIA) and paste your API key. DSH stores it.
|
|
18
|
+
2. In the chat box, type:
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
```
|
|
21
|
+
/greenharness models
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You get a numbered list of every model DSH knows:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
MODELS YOU CAN CHOOSE FROM
|
|
28
|
+
1. Nemotron 3 Ultra 550B A55B (NVIDIA, id nvidia/nemotron-3-ultra-550b-a55b)
|
|
29
|
+
2. Nemotron 3 Bolt E4B (NVIDIA, id nvidia/nemotron-3-bolt-e4b)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. Pick by number:
|
|
23
33
|
|
|
24
34
|
```
|
|
25
|
-
/greenharness
|
|
35
|
+
/greenharness small 2 the cheap model, for easy steps
|
|
36
|
+
/greenharness large 1 the strong model, for hard steps
|
|
26
37
|
```
|
|
27
38
|
|
|
28
|
-
|
|
39
|
+
You can also type part of the name instead of a number, for example `/greenharness large nvidia Nemotron 3 Ultra`. Optionally add a thinking level at the end: `low`, `medium`, `high`.
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
If you skip the large model, GreenHarness still saves tokens. It just won't switch models.
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
> **Important:** the command must be the only thing you send. Type it, then press Enter. If the chat shows "Thinking..." and runs commands, DSH treated your line as a normal message, which means the plugin isn't loaded. Update to the latest version and restart `dsh web`.
|
|
33
44
|
|
|
34
45
|
## 3. See it working
|
|
35
46
|
|
|
@@ -60,7 +71,9 @@ Model calls: 12. Switches between models: 2.
|
|
|
60
71
|
|---|---|
|
|
61
72
|
| `/greenharness` | Show the summary above |
|
|
62
73
|
| `/greenharness off` / `on` | Pause or resume everything |
|
|
63
|
-
| `/greenharness
|
|
74
|
+
| `/greenharness models` | List your models, numbered |
|
|
75
|
+
| `/greenharness large <number or name>` | Pick the strong model (turns switching on) |
|
|
76
|
+
| `/greenharness small <number or name>` | Pick the cheap model |
|
|
64
77
|
| `/greenharness routing off` | Stop switching models |
|
|
65
78
|
| `/greenharness distill off` | Stop shortening long output |
|
|
66
79
|
| `/greenharness help` | List every command |
|
|
@@ -69,8 +82,9 @@ Changes last until DSH restarts.
|
|
|
69
82
|
|
|
70
83
|
## 5. If something looks wrong
|
|
71
84
|
- **No `/greenharness` command:** run the install line again, then restart `dsh web`.
|
|
72
|
-
- **"Not set" for the strong model:** run `/greenharness
|
|
85
|
+
- **"Not set" for the strong model:** run `/greenharness models`, then `/greenharness large <number>`.
|
|
86
|
+
- **"DSH has no models to list":** add your key and models in Settings > Models first.
|
|
73
87
|
- **Needs Node 22.19 or newer.**
|
|
74
88
|
|
|
75
89
|
## Honest limits
|
|
76
|
-
The summary is text in the chat. DSH doesn't offer plugins a way to add their own panel, so there's no separate graphical dashboard. Savings numbers are estimates. The `/greenharness`
|
|
90
|
+
The summary is text in the chat. DSH doesn't offer plugins a way to add their own panel, so there's no separate graphical dashboard. Savings numbers are estimates. The `/greenharness models` and `large` commands are tested with simulated model lists, and the plugin loads in a real `dsh web`. I could not click through a live chat without a real API key.
|
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@ That is the whole setup. The package declares itself as a DSH bundle, so DSH tur
|
|
|
18
18
|
|---|---|
|
|
19
19
|
| `/greenharness` | Status: what is on, and what it has done in this process. |
|
|
20
20
|
| `/greenharness on` / `off` | Resume or pause every mechanism. |
|
|
21
|
-
| `/greenharness
|
|
21
|
+
| `/greenharness models` | List the models DSH knows, numbered. |
|
|
22
|
+
| `/greenharness large <number or name> [effort]` | Turn on model routing. The small model is whatever DSH already uses; this one is the large model that failing tests escalate to. |
|
|
23
|
+
| `/greenharness small <number or name>` | Set the small model. |
|
|
22
24
|
| `/greenharness small <provider> <model> [effort]` | Set the small model explicitly. |
|
|
23
25
|
| `/greenharness routing on` / `off` | Switch routing without losing the models. |
|
|
24
26
|
| `/greenharness distill` / `dedupe` / `loop` `on` / `off` | Switch one mechanism. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quadiusai/dsh-greenharness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "GreenHarness: a DeepSeek Harness (dsh) plugin that cuts token, GPU and dollar cost in multi-step agent workflows. Install with one command; control it with /greenharness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
package/src/control.mjs
CHANGED
|
@@ -11,26 +11,39 @@
|
|
|
11
11
|
const TOGGLES = new Set(['distill', 'dedupe', 'loop', 'routing'])
|
|
12
12
|
|
|
13
13
|
export const HELP = [
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
' /greenharness
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
' /greenharness
|
|
23
|
-
' /greenharness
|
|
24
|
-
' /greenharness
|
|
25
|
-
'
|
|
14
|
+
'GREENHARNESS: HOW TO USE IT',
|
|
15
|
+
'',
|
|
16
|
+
'It starts saving tokens on its own. These commands let you steer it.',
|
|
17
|
+
'',
|
|
18
|
+
'SEE WHAT IT IS DOING',
|
|
19
|
+
' /greenharness summary: tokens saved, progress bar, models',
|
|
20
|
+
'',
|
|
21
|
+
'CHOOSE MODELS (the cheap one for easy steps, a strong one for hard steps)',
|
|
22
|
+
' /greenharness models list the models you have set up in DSH, numbered',
|
|
23
|
+
' /greenharness large 3 use model number 3 from that list as the strong model',
|
|
24
|
+
' /greenharness large nvidia Nemotron 3 Ultra',
|
|
25
|
+
' or type part of its name, the provider first is optional',
|
|
26
|
+
' /greenharness small 1 set the cheap model (default: whatever DSH already uses)',
|
|
27
|
+
' Add API keys first in DSH: Settings > Models.',
|
|
28
|
+
'',
|
|
29
|
+
'TURN THINGS ON OR OFF',
|
|
30
|
+
' /greenharness on | off resume or pause everything',
|
|
31
|
+
' /greenharness routing on|off switching between the cheap and strong model',
|
|
32
|
+
' /greenharness distill on|off shorten very long command output',
|
|
33
|
+
' /greenharness dedupe on|off replace a repeated file read with a short pointer',
|
|
34
|
+
' /greenharness loop on|off stop a command that keeps failing the same way',
|
|
35
|
+
'',
|
|
36
|
+
'Settings last until DSH restarts.',
|
|
26
37
|
].join('\n')
|
|
27
38
|
|
|
39
|
+
const EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
40
|
+
|
|
28
41
|
/**
|
|
29
42
|
* Parse the text after `/greenharness`.
|
|
30
43
|
* @param {string} rawInput
|
|
31
44
|
* @returns {{ action: 'status' | 'help' } | { action: 'pause', paused: boolean }
|
|
32
45
|
* | { action: 'toggle', mechanism: string, enabled: boolean }
|
|
33
|
-
* | { action: 'tier', tier: 'small' | 'large',
|
|
46
|
+
* | { action: 'models' } | { action: 'tier', tier: 'small' | 'large', args: string[] }
|
|
34
47
|
* | { action: 'error', message: string }}
|
|
35
48
|
*/
|
|
36
49
|
export function parseCommand(rawInput = '') {
|
|
@@ -47,10 +60,8 @@ export function parseCommand(rawInput = '') {
|
|
|
47
60
|
if (args.length !== 1 || (value !== 'on' && value !== 'off')) return usage(`/greenharness ${verb} on|off`)
|
|
48
61
|
return { action: 'toggle', mechanism: verb, enabled: value === 'on' }
|
|
49
62
|
}
|
|
50
|
-
if (verb === '
|
|
51
|
-
|
|
52
|
-
return { action: 'tier', tier: verb, route: { provider: args[0], model: args[1], reasoningEffort: args[2] ?? '' } }
|
|
53
|
-
}
|
|
63
|
+
if (verb === 'models') return args.length === 0 ? { action: 'models' } : usage('/greenharness models')
|
|
64
|
+
if (verb === 'large' || verb === 'small') return { action: 'tier', tier: verb, args }
|
|
54
65
|
return { action: 'error', message: `Unknown command "${first}". Try /greenharness help.` }
|
|
55
66
|
}
|
|
56
67
|
|
|
@@ -58,9 +69,9 @@ export function parseCommand(rawInput = '') {
|
|
|
58
69
|
* Run one command and return the reply DSH shows.
|
|
59
70
|
* @param {string} rawInput
|
|
60
71
|
* @param {{ control: { paused: boolean }, settings: any, runtime: { stats(): Record<string, number> } }} host
|
|
61
|
-
* @returns {{ kind: 'success' | 'error', text: string }}
|
|
72
|
+
* @returns {Promise<{ kind: 'success' | 'error', text: string }>}
|
|
62
73
|
*/
|
|
63
|
-
export function runCommand(rawInput, host) {
|
|
74
|
+
export async function runCommand(rawInput, host) {
|
|
64
75
|
const command = parseCommand(rawInput)
|
|
65
76
|
switch (command.action) {
|
|
66
77
|
case 'error': return fail(command.message)
|
|
@@ -70,6 +81,7 @@ export function runCommand(rawInput, host) {
|
|
|
70
81
|
host.control.paused = command.paused
|
|
71
82
|
return ok(command.paused ? 'GreenHarness paused. Tool output and model routing are left alone.' : 'GreenHarness resumed.')
|
|
72
83
|
case 'toggle': return toggle(command, host)
|
|
84
|
+
case 'models': return showModels(host)
|
|
73
85
|
case 'tier': return setTier(command, host)
|
|
74
86
|
default: return fail('Unknown command. Try /greenharness help.')
|
|
75
87
|
}
|
|
@@ -104,9 +116,10 @@ export function statusText({ control, settings, runtime }) {
|
|
|
104
116
|
'',
|
|
105
117
|
`Models: small = ${small_}, large = ${describe(settings.tiers.large)}. Routing ${onOff(settings.routing.enabled)}.`,
|
|
106
118
|
`Features: shorten output ${onOff(settings.distill.enabled)}, repeated reads ${onOff(settings.readDedupe.enabled)}, loop stop ${onOff(settings.loopBreaker.enabled)}.`,
|
|
107
|
-
settings.tiers.large ?
|
|
119
|
+
...(settings.tiers.large ? [] : ['Next step: type /greenharness models, then /greenharness large <number> to add a strong model.']),
|
|
120
|
+
'All commands: /greenharness help',
|
|
108
121
|
)
|
|
109
|
-
return lines.
|
|
122
|
+
return lines.join('\n')
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
/** A 20-cell text progress bar. @param {number} fraction 0..1 */
|
|
@@ -129,14 +142,102 @@ function toggle({ mechanism, enabled }, { settings }) {
|
|
|
129
142
|
return ok(`${mechanism === 'loop' ? 'Loop breaker' : mechanism[0].toUpperCase() + mechanism.slice(1)} ${onOff(enabled)}.`)
|
|
130
143
|
}
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
const norm = text => String(text).toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Every model DSH knows about, numbered. Providers that cannot list models are skipped quietly.
|
|
149
|
+
* @param {{ llm?: any }} host
|
|
150
|
+
* @returns {Promise<{ n: number, provider: string, providerName: string, id: string, name: string }[]>}
|
|
151
|
+
*/
|
|
152
|
+
async function loadListing(host) {
|
|
153
|
+
const entries = []
|
|
154
|
+
if (host.llm === undefined) return entries
|
|
155
|
+
for (const provider of host.llm.listProviders()) {
|
|
156
|
+
let models = []
|
|
157
|
+
try {
|
|
158
|
+
models = await host.llm.listModels(provider.id)
|
|
159
|
+
} catch {
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
for (const model of models) {
|
|
163
|
+
entries.push({ n: entries.length + 1, provider: provider.id, providerName: provider.name, id: model.id, name: model.name })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
host.listing = entries
|
|
167
|
+
return entries
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const row = e => ` ${String(e.n).padStart(2)}. ${e.name} (${e.providerName}, id ${e.id})`
|
|
171
|
+
|
|
172
|
+
/** @param {{ llm?: any, listing?: any[] }} host */
|
|
173
|
+
async function showModels(host) {
|
|
174
|
+
const entries = await loadListing(host)
|
|
175
|
+
if (entries.length === 0) {
|
|
176
|
+
return ok([
|
|
177
|
+
'DSH has no models to list yet.',
|
|
178
|
+
'Add your API key and models first: open Settings > Models in DSH, then run /greenharness models again.',
|
|
179
|
+
].join('\n'))
|
|
180
|
+
}
|
|
181
|
+
return ok([
|
|
182
|
+
'MODELS YOU CAN CHOOSE FROM',
|
|
183
|
+
...entries.map(row),
|
|
184
|
+
'',
|
|
185
|
+
'Pick the strong one: /greenharness large <number> e.g. /greenharness large 3',
|
|
186
|
+
'Pick the cheap one: /greenharness small <number>',
|
|
187
|
+
].join('\n'))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Work out which model the user meant: a list number, or words from its name with an optional
|
|
192
|
+
* provider first and an optional reasoning effort last.
|
|
193
|
+
* @param {string[]} args
|
|
194
|
+
* @param {{ llm?: any, listing?: any[] }} host
|
|
195
|
+
* @returns {Promise<{ route: { provider: string, model: string, reasoningEffort: string }, label: string } | { error: string }>}
|
|
196
|
+
*/
|
|
197
|
+
async function resolveModel(args, host) {
|
|
198
|
+
let effort = ''
|
|
199
|
+
if (args.length > 1 && EFFORTS.has(args[args.length - 1].toLowerCase())) effort = args[args.length - 1].toLowerCase()
|
|
200
|
+
const words = effort === '' ? args : args.slice(0, -1)
|
|
201
|
+
if (words.length === 0) return { error: 'Which model? Run /greenharness models to see the list, then use its number.' }
|
|
202
|
+
const entries = host.listing?.length ? host.listing : await loadListing(host)
|
|
203
|
+
const pick = e => ({ route: { provider: e.provider, model: e.id, reasoningEffort: effort }, label: `${e.name} (${e.providerName})` })
|
|
204
|
+
if (words.length === 1 && /^\d+$/.test(words[0])) {
|
|
205
|
+
const hit = entries.find(e => e.n === Number(words[0]))
|
|
206
|
+
return hit ? pick(hit) : { error: `There is no model number ${words[0]}. Run /greenharness models to see the list.` }
|
|
207
|
+
}
|
|
208
|
+
if (host.llm === undefined) {
|
|
209
|
+
if (words.length < 2) return { error: 'Use: /greenharness large <provider> <model>' }
|
|
210
|
+
return { route: { provider: words[0], model: words.slice(1).join(' '), reasoningEffort: effort }, label: `${words[0]}/${words.slice(1).join(' ')}` }
|
|
211
|
+
}
|
|
212
|
+
const providers = host.llm.listProviders()
|
|
213
|
+
const owner = providers.find(p => norm(p.id) === norm(words[0]) || norm(p.name) === norm(words[0]))
|
|
214
|
+
const rest = owner ? words.slice(1) : words
|
|
215
|
+
const pool = owner ? entries.filter(e => e.provider === owner.id) : entries
|
|
216
|
+
const query = norm(rest.join(''))
|
|
217
|
+
if (query === '') return { error: `Which model from ${owner.name}? Run /greenharness models to see the list.` }
|
|
218
|
+
const exact = pool.filter(e => norm(e.id) === query || norm(e.name) === query)
|
|
219
|
+
const near = exact.length > 0 ? exact : pool.filter(e => norm(e.id).includes(query) || norm(e.name).includes(query))
|
|
220
|
+
if (near.length === 1) return pick(near[0])
|
|
221
|
+
if (near.length > 1) {
|
|
222
|
+
return { error: ['That matches several models. Pick one by number:', ...near.map(row)].join('\n') }
|
|
223
|
+
}
|
|
224
|
+
if (owner && rest.length === 1) {
|
|
225
|
+
return { route: { provider: owner.id, model: rest[0], reasoningEffort: effort }, label: `${rest[0]} (${owner.name}, typed id)` }
|
|
226
|
+
}
|
|
227
|
+
return { error: `I could not find a model matching "${rest.join(' ')}". Run /greenharness models to see what DSH has. Add keys and models in Settings > Models.` }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @param {{ tier: 'small' | 'large', args: string[] }} command @param {{ settings: any, llm?: any, listing?: any[] }} host */
|
|
231
|
+
async function setTier({ tier, args }, host) {
|
|
232
|
+
if (args.length === 0) return showModels(host)
|
|
233
|
+
const found = await resolveModel(args, host)
|
|
234
|
+
if ('error' in found) return fail(found.error)
|
|
235
|
+
host.settings.tiers[tier] = found.route
|
|
135
236
|
if (tier === 'large') {
|
|
136
|
-
settings.routing.enabled = true
|
|
137
|
-
return ok(`
|
|
237
|
+
host.settings.routing.enabled = true
|
|
238
|
+
return ok(`Strong model set to ${found.label}.\nSwitching is ON: GreenHarness will use it when the cheap model keeps failing tests, then go back.\nType /greenharness to watch it work.`)
|
|
138
239
|
}
|
|
139
|
-
return ok(`
|
|
240
|
+
return ok(`Cheap model set to ${found.label}.`)
|
|
140
241
|
}
|
|
141
242
|
|
|
142
243
|
/** @param {string} message */
|
package/src/index.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { runCommand } from './control.mjs'
|
|
|
25
25
|
export const name = 'greenharness'
|
|
26
26
|
|
|
27
27
|
/** Needs the command registry to offer `/greenharness`. */
|
|
28
|
-
export const inject = ['commands']
|
|
28
|
+
export const inject = ['commands', 'llm']
|
|
29
29
|
|
|
30
30
|
// Every field defaults to empty: an absent tier is an empty route, and normalizeSettings decides.
|
|
31
31
|
const route = z.object({
|
|
@@ -144,7 +144,7 @@ export function apply(ctx, config) {
|
|
|
144
144
|
}
|
|
145
145
|
})
|
|
146
146
|
|
|
147
|
-
registerCommand(ctx, { control, settings, runtime })
|
|
147
|
+
registerCommand(ctx, { control, settings, runtime, llm: ctx.llm })
|
|
148
148
|
|
|
149
149
|
ctx.on('session/event', (session, event) => {
|
|
150
150
|
try {
|
|
@@ -166,7 +166,8 @@ function registerCommand(ctx, host) {
|
|
|
166
166
|
yield ctx.commands.register({
|
|
167
167
|
definitionId: '@quadiusai/dsh-greenharness',
|
|
168
168
|
name: 'greenharness',
|
|
169
|
-
description: '
|
|
169
|
+
description: 'GreenHarness: see tokens saved, choose cheap and strong models, switch features (try: help)',
|
|
170
|
+
input: { hint: 'help | models | large <number> | small <number> | on | off' },
|
|
170
171
|
handler: (invocation) => runCommand(invocation.rawInput, host),
|
|
171
172
|
})
|
|
172
173
|
}, 'greenharness command')
|