@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.
@@ -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
- Use DSH's own screens:
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
- 1. In DSH web, open **Settings → Models**.
20
- 2. Add your provider (for example your NVIDIA/Brev endpoint) and paste your API key. DSH stores it for you.
21
- 3. Use DSH's model chooser to pick the **cheap (small)** model for normal chats.
22
- 4. Tell GreenHarness which model is the **strong (large)** one by typing in the chat box:
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 large <provider> <model>
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
- Example: `/greenharness large nemo-large lightning-30b`
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
- The cheap model is whatever DSH is already using. GreenHarness remembers it automatically. If you want to set it yourself: `/greenharness small <provider> <model>`.
41
+ If you skip the large model, GreenHarness still saves tokens. It just won't switch models.
31
42
 
32
- If you skip step 4, GreenHarness still saves tokens. It just won't switch models.
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 large <provider> <model>` | Pick the strong model (turns switching on) |
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 large ...` (step 2).
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` command in the web chat has been tested only through unit tests and DSH start-up, not by clicking through the live page.
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 large <provider> <model> [effort]` | Turn on model routing. The small model is whatever DSH already uses; this one is the large model that failing tests escalate to. |
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.0",
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
- 'GreenHarness commands (settings last until DSH restarts):',
15
- ' /greenharness status: what is on, and what it has done so far',
16
- ' /greenharness on | off resume or pause every mechanism',
17
- ' /greenharness large <provider> <model> [effort]',
18
- ' turn on model routing with this large model',
19
- ' /greenharness small <provider> <model> [effort]',
20
- ' set the small model (default: the model DSH uses)',
21
- ' /greenharness routing on | off',
22
- ' /greenharness distill on | off shorten long command output',
23
- ' /greenharness dedupe on | off replace a repeated file read with a pointer',
24
- ' /greenharness loop on | off block a repeated failing command',
25
- ' /greenharness help',
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', route: { provider: string, model: string, reasoningEffort: string } }
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 === 'large' || verb === 'small') {
51
- if (args.length < 2 || args.length > 3) return usage(`/greenharness ${verb} <provider> <model> [effort]`)
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 ? '' : 'Tip: pick a big model with /greenharness large <provider> <model> to save more.',
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.filter((l, i, a) => !(l === '' && (i === a.length - 1))).join('\n')
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
- /** @param {{ tier: 'small' | 'large', route: object }} command @param {{ settings: any }} host */
133
- function setTier({ tier, route }, { settings }) {
134
- settings.tiers[tier] = route
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(`Large model set to ${route.provider}/${route.model}. Routing is on: calls escalate to it after failing tests.`)
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(`Small model set to ${route.provider}/${route.model}.`)
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: 'Control GreenHarness: status, on/off, model routing, and each mechanism',
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')