pi-requesty-discover 1.2.0 → 2.0.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.adoc +14 -5
- package/README.md +14 -5
- package/package.json +1 -1
- package/src/index.ts +182 -87
package/README.adoc
CHANGED
|
@@ -16,7 +16,8 @@ This is a fork of https://github.com/requestyai/pi-requesty[pi-requesty] with ad
|
|
|
16
16
|
|
|
17
17
|
* Use `REQUESTY_API_KEY` from the environment
|
|
18
18
|
* Health-check available models and write a log about them
|
|
19
|
-
*
|
|
19
|
+
* Brief information about what was found, including added/removed models, health summary
|
|
20
|
+
* Confirm before writing `models.json`, with a dry-run mode that only previews
|
|
20
21
|
|
|
21
22
|
== When and why use it
|
|
22
23
|
|
|
@@ -25,7 +26,7 @@ Use this extension if
|
|
|
25
26
|
- you are on Requesty.ai
|
|
26
27
|
- you are limited to certain models by a company policy (for instance, for data residency/protection reasons)
|
|
27
28
|
- you don't want to handcraft your own curated list of models constantly
|
|
28
|
-
- you want to health
|
|
29
|
+
- you want to health-check models and preview the diff before writing
|
|
29
30
|
|
|
30
31
|
== Installation
|
|
31
32
|
|
|
@@ -124,12 +125,20 @@ Inside Pi:
|
|
|
124
125
|
/requesty-discover
|
|
125
126
|
----
|
|
126
127
|
|
|
127
|
-
The command
|
|
128
|
-
|
|
128
|
+
The command:
|
|
129
|
+
|
|
130
|
+
- discovers Requesty models (and optional health checks)
|
|
131
|
+
- shows a summary notification (added/removed IDs, health summary)
|
|
132
|
+
- asks for confirmation before writing `~/.pi/agent/models.json`
|
|
133
|
+
- reports whether the file was updated
|
|
134
|
+
|
|
135
|
+
If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
|
|
136
|
+
|
|
137
|
+
After a successful write, run `/reload`.
|
|
129
138
|
|
|
130
139
|
[source,text]
|
|
131
140
|
----
|
|
132
141
|
/requesty-discover --dry-run
|
|
133
142
|
----
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
|
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@ This is a fork of [pi-requesty](https://github.com/requestyai/pi-requesty) with
|
|
|
8
8
|
|
|
9
9
|
* Use `REQUESTY_API_KEY` from the environment
|
|
10
10
|
* Health-check available models and write a log about them
|
|
11
|
-
*
|
|
11
|
+
* Brief information about what was found, including added/removed models, health summary
|
|
12
|
+
* Confirm before writing `models.json`, with a dry-run mode that only previews
|
|
12
13
|
|
|
13
14
|
## When and why use it
|
|
14
15
|
|
|
@@ -17,7 +18,7 @@ Use this extension if
|
|
|
17
18
|
* you are on Requesty.ai
|
|
18
19
|
* you are limited to certain models by a company policy (for instance, for data residency/protection reasons)
|
|
19
20
|
* you don’t want to handcraft your own curated list of models constantly
|
|
20
|
-
* you want to health
|
|
21
|
+
* you want to health-check models and preview the diff before writing
|
|
21
22
|
|
|
22
23
|
## Installation
|
|
23
24
|
|
|
@@ -91,11 +92,19 @@ Inside Pi:
|
|
|
91
92
|
/requesty-discover
|
|
92
93
|
```
|
|
93
94
|
|
|
94
|
-
The command
|
|
95
|
-
|
|
95
|
+
The command:
|
|
96
|
+
|
|
97
|
+
* discovers Requesty models (and optional health checks)
|
|
98
|
+
* shows a summary notification (added/removed IDs, health summary)
|
|
99
|
+
* asks for confirmation before writing `~/.pi/agent/models.json`
|
|
100
|
+
* reports whether the file was updated
|
|
101
|
+
|
|
102
|
+
If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
|
|
103
|
+
|
|
104
|
+
After a successful write, run `/reload`.
|
|
96
105
|
|
|
97
106
|
```text
|
|
98
107
|
/requesty-discover --dry-run
|
|
99
108
|
```
|
|
100
109
|
|
|
101
|
-
|
|
110
|
+
Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ExtensionAPI, ExtensionCommandContext, ProviderModelConfig } from '@earendil-works/pi-coding-agent'
|
|
2
|
-
import { getEnv } from './env'
|
|
2
|
+
import { type Env, getEnv } from './env'
|
|
3
3
|
import {
|
|
4
4
|
diffModels,
|
|
5
5
|
formatModelsDiffSummary,
|
|
6
6
|
getRequestyConfig,
|
|
7
|
-
updateModelsJson,
|
|
8
7
|
type ModelsDiff,
|
|
8
|
+
updateModelsJson,
|
|
9
9
|
} from './models-json'
|
|
10
10
|
import { discoverModels } from './requesty-api'
|
|
11
11
|
import { checkModels, formatHealthSummary, writeHealthCheckLog } from './health-check'
|
|
@@ -26,105 +26,214 @@ export type Notifier = {
|
|
|
26
26
|
notify(message: string, level: NotificationLevel): void
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export type Confirmer = {
|
|
30
|
+
confirm(title: string, message: string): Promise<boolean>
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
export type StatusReporter = {
|
|
30
34
|
set(message: string): void
|
|
31
|
-
clear(): void
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
type ModelsJsonData = ReturnType<typeof getRequestyConfig>['data']
|
|
38
|
+
|
|
39
|
+
type DiscoveryEvaluation = {
|
|
40
|
+
dryRun: boolean
|
|
41
|
+
modelCount: number
|
|
42
|
+
failedCount: number
|
|
43
|
+
passing: ProviderModelConfig[]
|
|
44
|
+
diff: ModelsDiff
|
|
45
|
+
healthCheckSummary: string
|
|
46
|
+
logNote: string
|
|
47
|
+
data: ModelsJsonData
|
|
48
|
+
env: Env
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type EvaluationResult =
|
|
52
|
+
| {
|
|
53
|
+
ok: true
|
|
54
|
+
value: DiscoveryEvaluation
|
|
55
|
+
}
|
|
56
|
+
| { ok: false; error: unknown }
|
|
57
|
+
|
|
34
58
|
// noinspection JSUnusedGlobalSymbols
|
|
35
59
|
export default function (pi: ExtensionAPI) {
|
|
36
60
|
pi.registerCommand(COMMAND_NAME, {
|
|
37
61
|
description: 'Dynamically discover Requesty models, run health checks, and update the local models.json.',
|
|
38
62
|
getArgumentCompletions,
|
|
39
63
|
handler: async (args, ctx) => {
|
|
40
|
-
await
|
|
64
|
+
await runDiscoveryWorkflow(ctx, args)
|
|
41
65
|
},
|
|
42
66
|
})
|
|
43
67
|
}
|
|
44
68
|
|
|
69
|
+
export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, args: string) {
|
|
70
|
+
// Interactive TUI command; no print/json/rpc path.
|
|
71
|
+
if (ctx.mode !== 'tui') {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const notifier = createUiNotifier(ctx)
|
|
76
|
+
const confirmer = createUiConfirmer(ctx)
|
|
77
|
+
|
|
78
|
+
// Phase A: progress UI only. Loader must close before confirm (Phase B).
|
|
79
|
+
// Errors are wrapped because ctx.ui.custom resolves via done() and does not reject.
|
|
80
|
+
const evaluationResult: EvaluationResult = await runWithStatusUi(ctx, 'Discovering models...', async status => {
|
|
81
|
+
try {
|
|
82
|
+
const evaluation = await evaluateDiscovery(args, status)
|
|
83
|
+
return { ok: true, value: evaluation }
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return { ok: false, error }
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (!evaluationResult.ok) {
|
|
90
|
+
notifier.notify(formatDiscoveryFailure(evaluationResult.error), 'error')
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Phases B + C: decide, optional write, final notify — outside the loader.
|
|
95
|
+
await finalizeDiscovery(evaluationResult.value, confirmer, notifier)
|
|
96
|
+
}
|
|
97
|
+
|
|
45
98
|
async function runWithStatusUi<T>(
|
|
46
99
|
ctx: ExtensionCommandContext,
|
|
47
100
|
initialMessage: string,
|
|
48
|
-
fn: (status: StatusReporter
|
|
101
|
+
fn: (status: StatusReporter) => Promise<T>,
|
|
49
102
|
): Promise<T> {
|
|
50
|
-
if (ctx.mode !== 'tui') {
|
|
51
|
-
return fn(createNoopStatusReporter(), createNoopNotifier())
|
|
52
|
-
}
|
|
53
|
-
|
|
54
103
|
return ctx.ui.custom<T>((_tui, theme, _kb, done) => {
|
|
55
104
|
const loader = new RequestyStatusLoader(_tui, theme, initialMessage)
|
|
56
|
-
const notifier = createUiNotifier(ctx)
|
|
57
105
|
const status = createLoaderStatusReporter(loader)
|
|
58
106
|
void Promise.resolve()
|
|
59
|
-
.then(() => fn(status
|
|
107
|
+
.then(() => fn(status))
|
|
60
108
|
.then(done)
|
|
61
109
|
.catch(done)
|
|
62
110
|
return loader
|
|
63
111
|
})
|
|
64
112
|
}
|
|
65
113
|
|
|
66
|
-
|
|
114
|
+
function formatDiscoveryFailure(error: unknown): string {
|
|
115
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
116
|
+
return `Discovery failed: ${detail}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function evaluateDiscovery(args: string, status: StatusReporter): Promise<DiscoveryEvaluation> {
|
|
67
120
|
status.set('Discovering Requesty models...')
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
121
|
+
const dryRun = args.split(' ').includes(DRY_RUN_ARG)
|
|
122
|
+
|
|
123
|
+
const env = getEnv()
|
|
124
|
+
const { data, provider, existingModelIds } = getRequestyConfig(env)
|
|
125
|
+
const models = await discoverModels(provider)
|
|
126
|
+
const modelsMap = new Map(models.map(m => [m.id, m]))
|
|
127
|
+
|
|
128
|
+
let diff: ModelsDiff
|
|
129
|
+
let failedCount = 0
|
|
130
|
+
let passing: ProviderModelConfig[]
|
|
131
|
+
let logNote = ''
|
|
132
|
+
let healthCheckSummary = ''
|
|
133
|
+
|
|
134
|
+
if (env.health_check_mode !== 'off') {
|
|
135
|
+
status.set(`Checking models 0/${models.length}...`)
|
|
136
|
+
const healthResults = await checkModels(provider, models, env.health_check_mode === 'full', {
|
|
137
|
+
onProgress: ({ completed, total }) => {
|
|
138
|
+
status.set(`Checking models ${completed}/${total}...`)
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
|
|
142
|
+
failedCount = sortedResults.filter(r => !r.ok).length
|
|
143
|
+
passing = sortedResults.flatMap(r => {
|
|
144
|
+
const model = modelsMap.get(r.modelId)
|
|
145
|
+
return r.ok && model ? [model] : []
|
|
146
|
+
})
|
|
147
|
+
diff = diffModels(existingModelIds, passing)
|
|
148
|
+
healthCheckSummary = formatHealthSummary(sortedResults)
|
|
149
|
+
writeHealthCheckLog(provider, sortedResults, diff, env)
|
|
150
|
+
logNote = `Full health check log: ${env.health_check_log_path}\n`
|
|
151
|
+
} else {
|
|
152
|
+
passing = models
|
|
153
|
+
diff = diffModels(existingModelIds, passing)
|
|
72
154
|
}
|
|
73
155
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (env.health_check_mode !== 'off') {
|
|
87
|
-
status.set(`Checking models 0/${models.length}...`)
|
|
88
|
-
const healthResults = await checkModels(provider, models, env.health_check_mode === 'full', {
|
|
89
|
-
onProgress: ({ completed, total }) => {
|
|
90
|
-
status.set(`Checking models ${completed}/${total}...`)
|
|
91
|
-
},
|
|
92
|
-
})
|
|
93
|
-
const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
|
|
94
|
-
failed = sortedResults.filter(r => !r.ok)
|
|
95
|
-
passing = sortedResults.flatMap(r => {
|
|
96
|
-
const model = modelsMap.get(r.modelId)
|
|
97
|
-
return r.ok && model ? [model] : []
|
|
98
|
-
})
|
|
99
|
-
diff = diffModels(existingModelIds, passing)
|
|
100
|
-
healthCheckSummary = formatHealthSummary(sortedResults)
|
|
101
|
-
writeHealthCheckLog(provider, sortedResults, diff, env)
|
|
102
|
-
logNote = `Full health check log: ${env.health_check_log_path}\n`
|
|
103
|
-
} else {
|
|
104
|
-
passing = models
|
|
105
|
-
diff = diffModels(existingModelIds, passing)
|
|
106
|
-
}
|
|
156
|
+
return {
|
|
157
|
+
dryRun,
|
|
158
|
+
modelCount: models.length,
|
|
159
|
+
failedCount,
|
|
160
|
+
passing,
|
|
161
|
+
diff,
|
|
162
|
+
healthCheckSummary,
|
|
163
|
+
logNote,
|
|
164
|
+
data,
|
|
165
|
+
env,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
107
168
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
169
|
+
async function finalizeDiscovery(
|
|
170
|
+
evaluation: DiscoveryEvaluation,
|
|
171
|
+
confirmer: Confirmer,
|
|
172
|
+
notifier: Notifier,
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
const level = notificationLevel(evaluation)
|
|
175
|
+
const summary = buildDiscoverySummary(evaluation)
|
|
176
|
+
|
|
177
|
+
// Always surface the discovery result first (toast styling), then decide.
|
|
178
|
+
if (evaluation.dryRun) {
|
|
179
|
+
notifier.notify(
|
|
180
|
+
`${summary}
|
|
181
|
+
Dry run: left models.json unchanged.`,
|
|
182
|
+
level,
|
|
183
|
+
)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (evaluation.passing.length === 0) {
|
|
188
|
+
notifier.notify(
|
|
189
|
+
`${summary}
|
|
190
|
+
Left models.json unchanged.`,
|
|
191
|
+
level,
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
notifier.notify(summary, level)
|
|
197
|
+
const { title, message } = buildConfirmPrompt(evaluation)
|
|
198
|
+
const shouldUpdate = await confirmer.confirm(title, message)
|
|
199
|
+
if (shouldUpdate) {
|
|
200
|
+
updateModelsJson(evaluation.data, evaluation.passing, evaluation.env)
|
|
201
|
+
notifier.notify('Updated models.json. Run /reload to use the changes.', 'info')
|
|
202
|
+
return
|
|
203
|
+
}
|
|
112
204
|
|
|
113
|
-
|
|
114
|
-
|
|
205
|
+
notifier.notify('Left models.json unchanged.', 'info')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildDiscoverySummary(evaluation: DiscoveryEvaluation): string {
|
|
209
|
+
return [
|
|
210
|
+
`Discovered ${evaluation.modelCount} Requesty model(s).`,
|
|
211
|
+
evaluation.healthCheckSummary.trimEnd(),
|
|
212
|
+
formatModelsDiffSummary(evaluation.diff),
|
|
213
|
+
evaluation.logNote.trimEnd(),
|
|
214
|
+
]
|
|
215
|
+
.filter(part => part.length > 0)
|
|
216
|
+
.join('\n')
|
|
217
|
+
}
|
|
115
218
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
219
|
+
function buildConfirmPrompt(evaluation: DiscoveryEvaluation): { title: string; message: string } {
|
|
220
|
+
const hasIdChanges = evaluation.diff.added.length > 0 || evaluation.diff.removed.length > 0
|
|
221
|
+
if (hasIdChanges) {
|
|
222
|
+
return {
|
|
223
|
+
title: `Write ${evaluation.passing.length} model(s)?`,
|
|
224
|
+
message: 'Update models.json with the discovery result above.',
|
|
122
225
|
}
|
|
123
|
-
} catch (error) {
|
|
124
|
-
notifier.notify(`Discovery failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
|
125
|
-
} finally {
|
|
126
|
-
status.clear()
|
|
127
226
|
}
|
|
227
|
+
return {
|
|
228
|
+
title: 'Refresh models.json?',
|
|
229
|
+
message: 'No model ID changes. Rewrite the file to refresh metadata anyway?',
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function notificationLevel(evaluation: DiscoveryEvaluation): NotificationLevel {
|
|
234
|
+
if (evaluation.failedCount === 0) return 'info'
|
|
235
|
+
if (evaluation.failedCount < evaluation.modelCount) return 'warning'
|
|
236
|
+
return 'error'
|
|
128
237
|
}
|
|
129
238
|
|
|
130
239
|
function getArgumentCompletions(prefix: string): AutocompleteItem[] {
|
|
@@ -132,37 +241,26 @@ function getArgumentCompletions(prefix: string): AutocompleteItem[] {
|
|
|
132
241
|
{
|
|
133
242
|
value: DRY_RUN_ARG,
|
|
134
243
|
label: DRY_RUN_ARG,
|
|
135
|
-
description: 'Preview without
|
|
244
|
+
description: 'Preview discovery without offering to write models.json',
|
|
136
245
|
},
|
|
137
246
|
]
|
|
138
247
|
if (!prefix) return options
|
|
139
248
|
return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
140
249
|
}
|
|
141
250
|
|
|
142
|
-
function
|
|
143
|
-
return {
|
|
144
|
-
set() {
|
|
145
|
-
// Status output is only rendered in TUI mode.
|
|
146
|
-
},
|
|
147
|
-
clear() {
|
|
148
|
-
// Status output is only rendered in TUI mode.
|
|
149
|
-
},
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function createNoopNotifier(): Notifier {
|
|
251
|
+
function createUiNotifier(ctx: ExtensionCommandContext): Notifier {
|
|
154
252
|
return {
|
|
155
|
-
notify() {
|
|
156
|
-
|
|
253
|
+
notify(message: string, level?: NotificationLevel) {
|
|
254
|
+
const prefixedMessage = `${COMMAND_NAME}: ${message}`
|
|
255
|
+
ctx.ui.notify(prefixedMessage, level)
|
|
157
256
|
},
|
|
158
257
|
}
|
|
159
258
|
}
|
|
160
259
|
|
|
161
|
-
function
|
|
260
|
+
function createUiConfirmer(ctx: ExtensionCommandContext): Confirmer {
|
|
162
261
|
return {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
ctx.ui.notify(prefixedMessage, level)
|
|
262
|
+
confirm(title, message) {
|
|
263
|
+
return ctx.ui.confirm(title, message)
|
|
166
264
|
},
|
|
167
265
|
}
|
|
168
266
|
}
|
|
@@ -172,8 +270,5 @@ function createLoaderStatusReporter(loader: RequestyStatusLoader): StatusReporte
|
|
|
172
270
|
set(message: string) {
|
|
173
271
|
loader.setMessage(message)
|
|
174
272
|
},
|
|
175
|
-
clear() {
|
|
176
|
-
// Nothing to clear: the loader closes when ctx.ui.custom() resolves.
|
|
177
|
-
},
|
|
178
273
|
}
|
|
179
274
|
}
|