pi-requesty-discover 1.0.3 → 1.0.5

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 CHANGED
@@ -67,7 +67,7 @@ Example:
67
67
  "requesty-export": {
68
68
  "name": "Requesty",
69
69
  "baseUrl": "https://router.requesty.ai/v1",
70
- "apiKey": "rqsty-sk-...",
70
+ "apiKey": "$REQUESTY_API_KEY",
71
71
  "api": "openai-completions",
72
72
  "models": []
73
73
  }
@@ -78,6 +78,7 @@ Example:
78
78
  IMPORTANT: The `apiKey` from `models.json` is not used by this extension.
79
79
  It may contain Pi-specific environment-variable substitution that the extension cannot reliably resolve.
80
80
  Set `REQUESTY_API_KEY` instead. This variable is required.
81
+ You can configure the real `apiKey` as you like, but it would be recommended to use the same variable as for the model discovery.
81
82
 
82
83
  IMPORTANT: Health checks are designed to use only a few tokens, but they still make real Requesty API calls and may consume credits.
83
84
 
package/README.md CHANGED
@@ -55,7 +55,7 @@ Example:
55
55
  "requesty-export": {
56
56
  "name": "Requesty",
57
57
  "baseUrl": "https://router.requesty.ai/v1",
58
- "apiKey": "rqsty-sk-...",
58
+ "apiKey": "$REQUESTY_API_KEY",
59
59
  "api": "openai-completions",
60
60
  "models": []
61
61
  }
@@ -67,6 +67,7 @@ Example:
67
67
  The `apiKey` from `models.json` is not used by this extension.
68
68
  It may contain Pi-specific environment-variable substitution that the extension cannot reliably resolve.
69
69
  Set `REQUESTY_API_KEY` instead. This variable is required.
70
+ You can configure the real `apiKey` as you like, but it would be recommended to use the same variable as for the model discovery.
70
71
 
71
72
  **❗ IMPORTANT**\
72
73
  Health checks are designed to use only a few tokens, but they still make real Requesty API calls and may consume credits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-requesty-discover",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Discover available Requesty models for the Pi Coding Agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,7 +37,8 @@
37
37
  },
38
38
  "license": "MIT",
39
39
  "peerDependencies": {
40
- "@earendil-works/pi-coding-agent": "*"
40
+ "@earendil-works/pi-coding-agent": "*",
41
+ "@earendil-works/pi-tui": "*"
41
42
  },
42
43
  "pi": {
43
44
  "extensions": [
@@ -45,6 +46,7 @@
45
46
  ]
46
47
  },
47
48
  "devDependencies": {
49
+ "@earendil-works/pi-tui": "^0.80.3",
48
50
  "@eslint/js": "^10.0.1",
49
51
  "@types/node": "^26.1.0",
50
52
  "@vitest/coverage-v8": "^4.1.9",
package/src/index.ts CHANGED
@@ -1,13 +1,9 @@
1
- import {
2
- BorderedLoader,
3
- ExtensionAPI,
4
- ExtensionCommandContext,
5
- ProviderModelConfig,
6
- } from '@earendil-works/pi-coding-agent'
1
+ import { ExtensionAPI, ExtensionCommandContext, ProviderModelConfig } from '@earendil-works/pi-coding-agent'
7
2
  import { getEnv } from './env'
8
3
  import { getRequestyConfig, updateModelsJson } from './models-json'
9
4
  import { discoverModels } from './requesty-api'
10
5
  import { checkModels, formatHealthSummary, writeHealthCheckLog } from './health-check'
6
+ import { RequestyStatusLoader } from './ui/requesty-status-loader.ts'
11
7
 
12
8
  const COMMAND_NAME = 'requesty-discover'
13
9
  const DRY_RUN_ARG = '--dry-run'
@@ -18,7 +14,16 @@ interface AutocompleteItem {
18
14
  description: string
19
15
  }
20
16
 
21
- type NotificationLevel = 'info' | 'warning' | 'error'
17
+ export type NotificationLevel = 'info' | 'warning' | 'error'
18
+
19
+ export type Notifier = {
20
+ notify(message: string, level: NotificationLevel): void
21
+ }
22
+
23
+ export type StatusReporter = {
24
+ set(message: string): void
25
+ clear(): void
26
+ }
22
27
 
23
28
  // noinspection JSUnusedGlobalSymbols
24
29
  export default function (pi: ExtensionAPI) {
@@ -26,37 +31,38 @@ export default function (pi: ExtensionAPI) {
26
31
  description: 'Dynamically discover Requesty models, run health checks, and update the local models.json.',
27
32
  getArgumentCompletions,
28
33
  handler: async (args, ctx) => {
29
- await withLoader(ctx, 'Discovering models...', () => runCommand(args, ctx))
34
+ await runWithStatusUi(ctx, 'Discovering models...', (status, notifier) => runCommand(args, status, notifier))
30
35
  },
31
36
  })
32
37
  }
33
38
 
34
- async function withLoader<T>(ctx: ExtensionCommandContext, message: string, fn: () => Promise<T>): Promise<T> {
39
+ async function runWithStatusUi<T>(
40
+ ctx: ExtensionCommandContext,
41
+ initialMessage: string,
42
+ fn: (status: StatusReporter, notifier: Notifier) => Promise<T>,
43
+ ): Promise<T> {
44
+ if (ctx.mode !== 'tui') {
45
+ return fn(createNoopStatusReporter(), createNoopNotifier())
46
+ }
47
+
35
48
  return ctx.ui.custom<T>((_tui, theme, _kb, done) => {
36
- const loader = new BorderedLoader(_tui, theme, message, { cancellable: false })
37
- fn().then(done).catch(done)
49
+ const loader = new RequestyStatusLoader(_tui, theme, initialMessage)
50
+ const notifier = createUiNotifier(ctx)
51
+ const status = createLoaderStatusReporter(loader)
52
+ void Promise.resolve()
53
+ .then(() => fn(status, notifier))
54
+ .then(done)
55
+ .catch(done)
38
56
  return loader
39
57
  })
40
58
  }
41
59
 
42
- function getArgumentCompletions(prefix: string): AutocompleteItem[] {
43
- const options = [
44
- {
45
- value: DRY_RUN_ARG,
46
- label: DRY_RUN_ARG,
47
- description: 'Preview without writing into the new model.json file',
48
- },
49
- ]
50
- if (!prefix) return options
51
- return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
52
- }
53
-
54
- async function runCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
55
- ctx.ui.setStatus(COMMAND_NAME, 'Discovering Requesty models...')
60
+ export async function runCommand(args: string, status: StatusReporter, notifier: Notifier): Promise<void> {
61
+ status.set('Discovering Requesty models...')
56
62
  const parts = args.split(' ')
57
63
  const dryRun = parts.includes(DRY_RUN_ARG)
58
64
  if (dryRun) {
59
- notify(ctx, 'running in dry mode, no changes will be done')
65
+ notifier.notify('running in dry mode, no changes will be done', 'info')
60
66
  }
61
67
 
62
68
  try {
@@ -71,10 +77,10 @@ async function runCommand(args: string, ctx: ExtensionCommandContext): Promise<v
71
77
  let healthCheckSummary = ''
72
78
 
73
79
  if (env.health_check_mode !== 'off') {
74
- ctx.ui.setStatus(COMMAND_NAME, `Checking models 0/${models.length}...`)
80
+ status.set(`Checking models 0/${models.length}...`)
75
81
  const healthResults = await checkModels(provider, models, env.health_check_mode === 'full', {
76
82
  onProgress: ({ completed, total }) => {
77
- ctx.ui.setStatus(COMMAND_NAME, `Checking models ${completed}/${total}...`)
83
+ status.set(`Checking models ${completed}/${total}...`)
78
84
  },
79
85
  })
80
86
  const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
@@ -99,20 +105,66 @@ async function runCommand(args: string, ctx: ExtensionCommandContext): Promise<v
99
105
  const message = `Discovered ${models.length} Requesty model(s).\n${healthCheckSummary}${logNote}${writeNote}`
100
106
 
101
107
  if (failed.length === 0) {
102
- notify(ctx, message, 'info')
108
+ notifier.notify(message, 'info')
103
109
  } else if (failed.length < models.length) {
104
- notify(ctx, message, 'warning')
110
+ notifier.notify(message, 'warning')
105
111
  } else {
106
- notify(ctx, message, 'error')
112
+ notifier.notify(message, 'error')
107
113
  }
108
114
  } catch (error) {
109
- notify(ctx, `Discovery failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
115
+ notifier.notify(`Discovery failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
110
116
  } finally {
111
- ctx.ui.setStatus(COMMAND_NAME, undefined)
117
+ status.clear()
118
+ }
119
+ }
120
+
121
+ function getArgumentCompletions(prefix: string): AutocompleteItem[] {
122
+ const options = [
123
+ {
124
+ value: DRY_RUN_ARG,
125
+ label: DRY_RUN_ARG,
126
+ description: 'Preview without writing into the new model.json file',
127
+ },
128
+ ]
129
+ if (!prefix) return options
130
+ return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
131
+ }
132
+
133
+ function createNoopStatusReporter(): StatusReporter {
134
+ return {
135
+ set() {
136
+ // Status output is only rendered in TUI mode.
137
+ },
138
+ clear() {
139
+ // Status output is only rendered in TUI mode.
140
+ },
112
141
  }
113
142
  }
114
143
 
115
- function notify(ctx: ExtensionCommandContext, message: string, level?: NotificationLevel): void {
116
- const prefixedMessage = `${COMMAND_NAME}: ${message}`
117
- ctx.ui.notify(prefixedMessage, level)
144
+ function createNoopNotifier(): Notifier {
145
+ return {
146
+ notify() {
147
+ // Notifications are only rendered in TUI mode.
148
+ },
149
+ }
150
+ }
151
+
152
+ function createUiNotifier(ctx: ExtensionCommandContext): Notifier {
153
+ return {
154
+ notify(message: string, level?: NotificationLevel) {
155
+ const prefixedMessage = `${COMMAND_NAME}: ${message}`
156
+ ctx.ui.notify(prefixedMessage, level)
157
+ },
158
+ }
159
+ }
160
+
161
+ function createLoaderStatusReporter(loader: RequestyStatusLoader): StatusReporter {
162
+ return {
163
+ set(message: string) {
164
+ loader.setMessage(message)
165
+ },
166
+ clear() {
167
+ // Nothing to clear: the loader closes when ctx.ui.custom() resolves.
168
+ },
169
+ }
118
170
  }
@@ -56,8 +56,10 @@ export function getRequestyConfig(envConfig: Env = getEnv()): RequestyConfig {
56
56
  }
57
57
 
58
58
  export function updateModelsJson(data: ModelsJson, models: ProviderModelConfig[], envConfig: Env = getEnv()): void {
59
+ const provider = data.providers[envConfig.provider_id]
59
60
  data.providers[envConfig.provider_id] = {
60
- ...data.providers[envConfig.provider_id],
61
+ ...provider,
62
+ apiKey: nonEmptyString(provider.apiKey) ?? '$REQUESTY_API_KEY',
61
63
  models: models.map(model => ({
62
64
  id: model.id,
63
65
  name: model.name,
@@ -0,0 +1,38 @@
1
+ import { DynamicBorder, Theme } from '@earendil-works/pi-coding-agent'
2
+ import { Container, Loader, Spacer, TUI } from '@earendil-works/pi-tui'
3
+
4
+ /**
5
+ * A loader that allows us to change the displayed message.
6
+ *
7
+ * @remarks
8
+ * this is essentially a reimplementation of the default BorderedLoader from pi-tui
9
+ * which does not offer a public api for changing its message.
10
+ */
11
+ export class RequestyStatusLoader extends Container {
12
+ private readonly loader: Loader
13
+
14
+ constructor(tui: TUI, theme: Theme, message: string) {
15
+ super()
16
+
17
+ const borderColor = (s: string) => theme.fg('border', s)
18
+ this.loader = new Loader(
19
+ tui,
20
+ s => theme.fg('accent', s),
21
+ s => theme.fg('muted', s),
22
+ message,
23
+ )
24
+
25
+ this.addChild(new DynamicBorder(borderColor))
26
+ this.addChild(this.loader)
27
+ this.addChild(new Spacer(1))
28
+ this.addChild(new DynamicBorder(borderColor))
29
+ }
30
+
31
+ setMessage(message: string): void {
32
+ this.loader.setMessage(message)
33
+ }
34
+
35
+ dispose(): void {
36
+ this.loader.stop()
37
+ }
38
+ }