pi-requesty-discover 2.1.0 → 2.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/docs/DECISIONS.adoc +16 -0
- package/package.json +1 -1
- package/src/index.ts +76 -50
- package/src/models-json.ts +1 -1
- package/src/requesty-api.ts +9 -2
package/docs/DECISIONS.adoc
CHANGED
|
@@ -14,6 +14,22 @@ token/model state. No "extra footer" API exists.
|
|
|
14
14
|
Decision: keep `setStatus`. Revisit when the line overflows visibly with other
|
|
15
15
|
extensions' statuses, or if the usage string grows past a typical width.
|
|
16
16
|
|
|
17
|
+
== Hand-rolled `Try<T>` instead of an effects library
|
|
18
|
+
|
|
19
|
+
Context: env loading and discovery evaluation both need a caught-error result value
|
|
20
|
+
(`{ ok: true, value } | { ok: false, error }`). Libraries like Effect ship an
|
|
21
|
+
industrial `Either`/`Effect.try` for exactly this.
|
|
22
|
+
|
|
23
|
+
Why not: Effect is a runtime, a generator DSL, and a worldview — not a utility.
|
|
24
|
+
Two call sites in a Pi extension don't justify the dependency weight or the
|
|
25
|
+
learning curve for contributors. The hand-rolled version is ~10 lines
|
|
26
|
+
(`Try<T>`, `runCatching`, `runCatchingAsync`, named after Kotlin's `Result`),
|
|
27
|
+
lives in `src/index.ts`, and documents intent just as well.
|
|
28
|
+
|
|
29
|
+
Decision: keep the hand-rolled `Try<T>`. No `monads.ts` module either — rule of
|
|
30
|
+
three. Revisit if the extension ever needs typed errors, retries, or resource
|
|
31
|
+
scoping; that's the day Effect earns its place.
|
|
32
|
+
|
|
17
33
|
== Not a provider — a models.json-writing extension
|
|
18
34
|
|
|
19
35
|
Context: Requesty already works as a Pi provider via `models.json`. The temptation
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -51,43 +51,42 @@ type DiscoveryEvaluation = {
|
|
|
51
51
|
healthCheckSummary: string
|
|
52
52
|
logNote: string
|
|
53
53
|
data: ModelsJsonData
|
|
54
|
-
env: Env
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
type
|
|
58
|
-
| {
|
|
59
|
-
ok: true
|
|
60
|
-
value: DiscoveryEvaluation
|
|
61
|
-
}
|
|
62
|
-
| { ok: false; error: unknown }
|
|
56
|
+
export type Try<T> = { ok: true; value: T } | { ok: false; error: unknown }
|
|
63
57
|
|
|
64
|
-
//
|
|
58
|
+
// token suppresses stale writes when turns overlap
|
|
65
59
|
let latestToken: object = {}
|
|
66
60
|
|
|
67
61
|
// noinspection JSUnusedGlobalSymbols
|
|
68
62
|
export default function (pi: ExtensionAPI) {
|
|
63
|
+
const env = runCatching(() => getEnv())
|
|
64
|
+
|
|
69
65
|
pi.registerCommand(COMMAND_NAME, {
|
|
70
66
|
description: 'Dynamically discover Requesty models, run health checks, and update the local models.json.',
|
|
71
67
|
getArgumentCompletions,
|
|
72
68
|
handler: async (args, ctx) => {
|
|
73
|
-
await runDiscoveryWorkflow(ctx, args)
|
|
69
|
+
await runDiscoveryWorkflow(ctx, env, args)
|
|
74
70
|
},
|
|
75
71
|
})
|
|
76
72
|
|
|
77
73
|
pi.on('turn_end', (_event, ctx) => {
|
|
78
|
-
void updateUsageStatus(ctx)
|
|
74
|
+
void updateUsageStatus(ctx, env)
|
|
79
75
|
})
|
|
80
76
|
|
|
81
77
|
pi.on('session_start', (_event, ctx) => {
|
|
82
|
-
|
|
78
|
+
complainOnBrokenEnv(ctx, env)
|
|
79
|
+
void updateUsageStatus(ctx, env)
|
|
83
80
|
})
|
|
84
81
|
|
|
85
82
|
pi.on('model_select', (_event, ctx) => {
|
|
86
|
-
void updateUsageStatus(ctx)
|
|
83
|
+
void updateUsageStatus(ctx, env)
|
|
87
84
|
})
|
|
88
85
|
}
|
|
89
86
|
|
|
90
|
-
export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, args: string) {
|
|
87
|
+
export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, env: Try<Env>, args: string) {
|
|
88
|
+
complainOnBrokenEnv(ctx, env)
|
|
89
|
+
if (!env.ok) return
|
|
91
90
|
// Interactive TUI command; no print/json/rpc path.
|
|
92
91
|
if (ctx.mode !== 'tui') {
|
|
93
92
|
return
|
|
@@ -98,14 +97,11 @@ export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, args: s
|
|
|
98
97
|
|
|
99
98
|
// Phase A: progress UI only. Loader must close before confirm (Phase B).
|
|
100
99
|
// Errors are wrapped because ctx.ui.custom resolves via done() and does not reject.
|
|
101
|
-
const evaluationResult:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return { ok: false, error }
|
|
107
|
-
}
|
|
108
|
-
})
|
|
100
|
+
const evaluationResult: Try<DiscoveryEvaluation> = await runWithStatusUi(
|
|
101
|
+
ctx,
|
|
102
|
+
'Discovering models...',
|
|
103
|
+
async status => await runCatchingAsync(() => evaluateDiscovery(args, env.value, status)),
|
|
104
|
+
)
|
|
109
105
|
|
|
110
106
|
if (!evaluationResult.ok) {
|
|
111
107
|
notifier.notify(formatDiscoveryFailure(evaluationResult.error), 'error')
|
|
@@ -113,7 +109,12 @@ export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, args: s
|
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
// Phases B + C: decide, optional write, final notify — outside the loader.
|
|
116
|
-
await finalizeDiscovery(evaluationResult.value, confirmer, notifier)
|
|
112
|
+
await finalizeDiscovery(evaluationResult.value, confirmer, notifier, env.value)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatDiscoveryFailure(error: unknown): string {
|
|
116
|
+
const detail = formatError(error)
|
|
117
|
+
return `Discovery failed: ${detail}`
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
async function runWithStatusUi<T>(
|
|
@@ -132,16 +133,10 @@ async function runWithStatusUi<T>(
|
|
|
132
133
|
})
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
function
|
|
136
|
-
const detail = error instanceof Error ? error.message : String(error)
|
|
137
|
-
return `Discovery failed: ${detail}`
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function evaluateDiscovery(args: string, status: StatusReporter): Promise<DiscoveryEvaluation> {
|
|
136
|
+
async function evaluateDiscovery(args: string, env: Env, status: StatusReporter): Promise<DiscoveryEvaluation> {
|
|
141
137
|
status.set('Discovering Requesty models...')
|
|
142
138
|
const dryRun = args.split(' ').includes(DRY_RUN_ARG)
|
|
143
139
|
|
|
144
|
-
const env = getEnv()
|
|
145
140
|
const { data, provider, existingModelIds } = getRequestyConfig(env)
|
|
146
141
|
const models = await discoverModels(provider)
|
|
147
142
|
const modelsMap = new Map(models.map(m => [m.id, m]))
|
|
@@ -183,7 +178,6 @@ async function evaluateDiscovery(args: string, status: StatusReporter): Promise<
|
|
|
183
178
|
healthCheckSummary,
|
|
184
179
|
logNote,
|
|
185
180
|
data,
|
|
186
|
-
env,
|
|
187
181
|
}
|
|
188
182
|
}
|
|
189
183
|
|
|
@@ -191,6 +185,7 @@ async function finalizeDiscovery(
|
|
|
191
185
|
evaluation: DiscoveryEvaluation,
|
|
192
186
|
confirmer: Confirmer,
|
|
193
187
|
notifier: Notifier,
|
|
188
|
+
env: Env,
|
|
194
189
|
): Promise<void> {
|
|
195
190
|
const level = notificationLevel(evaluation)
|
|
196
191
|
const summary = buildDiscoverySummary(evaluation)
|
|
@@ -218,7 +213,7 @@ Left models.json unchanged.`,
|
|
|
218
213
|
const { title, message } = buildConfirmPrompt(evaluation)
|
|
219
214
|
const shouldUpdate = await confirmer.confirm(title, message)
|
|
220
215
|
if (shouldUpdate) {
|
|
221
|
-
updateModelsJson(evaluation.data, evaluation.passing,
|
|
216
|
+
updateModelsJson(evaluation.data, evaluation.passing, env)
|
|
222
217
|
notifier.notify('Updated models.json. Run /reload to use the changes.', 'info')
|
|
223
218
|
return
|
|
224
219
|
}
|
|
@@ -269,7 +264,7 @@ function getArgumentCompletions(prefix: string): AutocompleteItem[] {
|
|
|
269
264
|
return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
270
265
|
}
|
|
271
266
|
|
|
272
|
-
function createUiNotifier(ctx:
|
|
267
|
+
function createUiNotifier(ctx: ExtensionContext): Notifier {
|
|
273
268
|
return {
|
|
274
269
|
notify(message: string, level?: NotificationLevel) {
|
|
275
270
|
const prefixedMessage = `${COMMAND_NAME}: ${message}`
|
|
@@ -278,7 +273,7 @@ function createUiNotifier(ctx: ExtensionCommandContext): Notifier {
|
|
|
278
273
|
}
|
|
279
274
|
}
|
|
280
275
|
|
|
281
|
-
function createUiConfirmer(ctx:
|
|
276
|
+
function createUiConfirmer(ctx: ExtensionContext): Confirmer {
|
|
282
277
|
return {
|
|
283
278
|
confirm(title, message) {
|
|
284
279
|
return ctx.ui.confirm(title, message)
|
|
@@ -294,19 +289,18 @@ function createLoaderStatusReporter(loader: RequestyStatusLoader): StatusReporte
|
|
|
294
289
|
}
|
|
295
290
|
}
|
|
296
291
|
|
|
297
|
-
|
|
292
|
+
async function updateUsageStatus(ctx: ExtensionContext, env: Try<Env>): Promise<void> {
|
|
293
|
+
if (!env.ok) return
|
|
298
294
|
if (!ctx.hasUI) return // no footer to write to (print/json mode): skip the wasted fetch
|
|
299
295
|
const token: object = {}
|
|
300
296
|
latestToken = token
|
|
301
|
-
const
|
|
302
|
-
if (providerId === undefined) return // not configured for Requesty: nothing to do
|
|
303
|
-
const shouldClear = ctx.model?.provider !== providerId
|
|
297
|
+
const shouldClear = ctx.model?.provider !== env.value.provider_id
|
|
304
298
|
try {
|
|
305
299
|
if (shouldClear) {
|
|
306
300
|
ctx.ui.setStatus(USAGE_STATUS_KEY, undefined)
|
|
307
301
|
return
|
|
308
302
|
}
|
|
309
|
-
const info = await fetchUsageStatus()
|
|
303
|
+
const info = await fetchUsageStatus(env.value)
|
|
310
304
|
if (latestToken !== token) return
|
|
311
305
|
ctx.ui.setStatus(USAGE_STATUS_KEY, formatUsageStatus(info))
|
|
312
306
|
} catch {
|
|
@@ -314,15 +308,6 @@ export async function updateUsageStatus(ctx: ExtensionContext): Promise<void> {
|
|
|
314
308
|
}
|
|
315
309
|
}
|
|
316
310
|
|
|
317
|
-
/** Returns the configured Requesty provider id, or undefined when Requesty is not configured. */
|
|
318
|
-
function getRequestyProviderId(): string | undefined {
|
|
319
|
-
try {
|
|
320
|
-
return getEnv().provider_id
|
|
321
|
-
} catch {
|
|
322
|
-
return undefined
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
311
|
export function formatUsageStatus(info: ApiKeyInfo): string {
|
|
327
312
|
const spend = `$${info.monthlySpend.toFixed(2)}`
|
|
328
313
|
if (info.monthlyLimit <= 0) {
|
|
@@ -333,8 +318,49 @@ export function formatUsageStatus(info: ApiKeyInfo): string {
|
|
|
333
318
|
return `Requesty Usage (${info.name}): ${spend}/${limit} (${percent}%)`
|
|
334
319
|
}
|
|
335
320
|
|
|
336
|
-
|
|
337
|
-
|
|
321
|
+
let lastFetched: { value: ApiKeyInfo; time: Date } | undefined
|
|
322
|
+
|
|
323
|
+
async function fetchUsageStatus(env: Env): Promise<ApiKeyInfo> {
|
|
324
|
+
const now = new Date()
|
|
325
|
+
if (lastFetched?.time && now.getTime() - lastFetched.time.getTime() < 2000) {
|
|
326
|
+
return lastFetched.value
|
|
327
|
+
}
|
|
338
328
|
const { provider } = getRequestyConfig(env)
|
|
339
|
-
|
|
329
|
+
const value = await fetchApiUsage(provider)
|
|
330
|
+
lastFetched = { value, time: new Date() }
|
|
331
|
+
return value
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function formatError(error: unknown) {
|
|
335
|
+
return error instanceof Error ? error.message : String(error)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* For testing purposes, reset the cache after each run
|
|
340
|
+
*/
|
|
341
|
+
export function resetUsageStatusCache(): void {
|
|
342
|
+
lastFetched = undefined
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function runCatching<T>(fn: () => T): Try<T> {
|
|
346
|
+
try {
|
|
347
|
+
return { ok: true, value: fn() }
|
|
348
|
+
} catch (error) {
|
|
349
|
+
return { ok: false, error }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function runCatchingAsync<T>(fn: () => Promise<T>): Promise<Try<T>> {
|
|
354
|
+
try {
|
|
355
|
+
return { ok: true, value: await fn() }
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return { ok: false, error }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function complainOnBrokenEnv(ctx: ExtensionContext, env: Try<Env>) {
|
|
362
|
+
if (!env.ok) {
|
|
363
|
+
const notifier = createUiNotifier(ctx)
|
|
364
|
+
notifier.notify(`failed to load env: ${formatError(env.error)}`, 'error')
|
|
365
|
+
}
|
|
340
366
|
}
|
package/src/models-json.ts
CHANGED
|
@@ -25,7 +25,7 @@ const ModelsJsonSchema = z
|
|
|
25
25
|
type ModelsJson = z.infer<typeof ModelsJsonSchema>
|
|
26
26
|
type ModelsJsonProvider = z.infer<typeof ProviderSchema>
|
|
27
27
|
|
|
28
|
-
type RequestyProvider = ModelsJsonProvider & {
|
|
28
|
+
export type RequestyProvider = ModelsJsonProvider & {
|
|
29
29
|
name: string
|
|
30
30
|
baseUrl: string
|
|
31
31
|
apiKey: string
|
package/src/requesty-api.ts
CHANGED
|
@@ -31,9 +31,14 @@ type Provider = {
|
|
|
31
31
|
apiKey: string
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export async function discoverModels(
|
|
34
|
+
export async function discoverModels(
|
|
35
|
+
provider: Provider,
|
|
36
|
+
options: { timeoutMs?: number } = {},
|
|
37
|
+
): Promise<ProviderModelConfig[]> {
|
|
38
|
+
const timeoutMs = options.timeoutMs ?? 5_000
|
|
35
39
|
const response = await fetch(`${provider.baseUrl}/models`, {
|
|
36
40
|
headers: { Authorization: `Bearer ${provider.apiKey}` },
|
|
41
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
37
42
|
})
|
|
38
43
|
|
|
39
44
|
if (!response.ok) {
|
|
@@ -82,10 +87,12 @@ const ApiKeyInfoSchema = z
|
|
|
82
87
|
|
|
83
88
|
export type ApiKeyInfo = z.infer<typeof ApiKeyInfoSchema>
|
|
84
89
|
|
|
85
|
-
export async function fetchApiUsage(provider: Provider): Promise<ApiKeyInfo> {
|
|
90
|
+
export async function fetchApiUsage(provider: Provider, options: { timeoutMs?: number } = {}): Promise<ApiKeyInfo> {
|
|
86
91
|
const url = `${REQUESTY_MANAGE_URL}/apikey/self`
|
|
92
|
+
const timeoutMs = options.timeoutMs ?? 2_000
|
|
87
93
|
const response = await fetch(url, {
|
|
88
94
|
headers: { Authorization: `Bearer ${provider.apiKey}` },
|
|
95
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
89
96
|
})
|
|
90
97
|
|
|
91
98
|
if (!response.ok) {
|