pi-requesty-discover 2.1.1 → 3.0.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/README.adoc CHANGED
@@ -8,17 +8,19 @@ endif::[]
8
8
 
9
9
  = pi-requesty-discover
10
10
 
11
- image:https://img.shields.io/npm/v/pi-requesty-discover.svg[npm version, link=https://www.npmjs.com/package/pi-requesty-discover]
11
+ image:https://img.shields.io/npm/v/pi-requesty-discover.svg[npm version,link=https://www.npmjs.com/package/pi-requesty-discover]
12
12
 
13
13
  This extension helps you discover currently available models in requesty.ai for your Pi Coding Agent.
14
14
 
15
15
  This is a fork of https://github.com/requestyai/pi-requesty[pi-requesty] with additional features:
16
16
 
17
- * Use `REQUESTY_API_KEY` from the environment
17
+ * Use the `apiKey` from the model registry allowing you to use
18
+ https://pi.dev/docs/latest/models#value-resolution[pi's full value resolution] capabilities
18
19
  * Health-check available models and write a log about them
19
20
  * Brief information about what was found, including added/removed models, health summary
20
21
  * Confirm before writing `models.json`, with a dry-run mode that only previews
21
22
  * A footer status line showing your monthly Requesty spend against your monthly limit
23
+ * Optional Settings file in *json5* format
22
24
 
23
25
  == When and why use it
24
26
 
@@ -59,7 +61,7 @@ pi -e ./pi-requesty-discover
59
61
  == Configuration
60
62
 
61
63
  The extension reads the `requesty-export` provider from `~/.pi/agent/models.json`.
62
- You can override the provider id with the `REQUESTY_PROVIDER_ID` variable.
64
+ You can override the provider id via the settings.
63
65
 
64
66
  Example:
65
67
 
@@ -70,46 +72,47 @@ Example:
70
72
  "requesty-export": {
71
73
  "name": "Requesty",
72
74
  "baseUrl": "https://router.requesty.ai/v1",
73
- "apiKey": "$REQUESTY_API_KEY",
74
75
  "api": "openai-completions",
76
+ "apiKey": "$REQUESTY_API_KEY",
75
77
  "models": []
76
78
  }
77
79
  }
78
80
  }
79
81
  ----
80
82
 
81
- IMPORTANT: The `apiKey` from `models.json` is not used by this extension.
82
- It may contain Pi-specific environment-variable substitution that the extension cannot reliably resolve.
83
- Set `REQUESTY_API_KEY` instead. This variable is required.
84
- You can configure the real `apiKey` as you like, but it would be recommended to use the same variable as for the model discovery.
83
+ IMPORTANT: Health checks are designed to use only a few tokens, but they still make real Requesty API calls and **will** consume credits.
85
84
 
86
- IMPORTANT: Health checks are designed to use only a few tokens, but they still make real Requesty API calls and may consume credits.
85
+ On startup, the extension fetches `<baseUrl>/models` using the configured `apiKey` as the bearer token and registers discovered and healthy models with Pi.
87
86
 
88
- On startup, the extension fetches `<baseUrl>/models` using `REQUESTY_API_KEY` as the bearer token and registers discovered and healthy models with Pi.
87
+ === Discovery settings
89
88
 
90
- === Environment variables
89
+ The discovery workflow reads optional settings from `requesty-discovery-settings.json5` in the Pi config directory (`PI_CODING_AGENT_DIR`).
90
+ The file uses JSON5, so it supports comments. It is optional; missing settings default to empty.
91
91
 
92
- [cols="1,1,1,1,2", options="header"]
93
- |===
94
- | Name | Required | Type | Default | Meaning
92
+ Example:
95
93
 
96
- | `REQUESTY_API_KEY`
97
- | yes
98
- | `string`
99
- |
100
- | Requesty API key used for model discovery and health checks.
94
+ [source,json5]
95
+ ----
96
+ {
97
+ // override the default provider id
98
+ providerId: "requesty-export",
99
+ // configure the health check mode
100
+ healthCheckMode: "full", // or "basic", "off"
101
+ // models excluded from discovery; useful for models that keep failing health checks
102
+ bannedModels: [
103
+ // hangs on tool calls
104
+ 'requesty/unstable-model',
105
+ ],
106
+ }
107
+ ----
101
108
 
102
- | `REQUESTY_PROVIDER_ID`
103
- | no
104
- | `string`
105
- | `requesty-export`
106
- | The id of the provider.
109
+ Banned models are excluded before health checks run (so they cost no credits) and are removed from `models.json` on the next discovery write.
107
110
 
108
- | `REQUESTY_HEALTH_CHECK_MODE`
109
- | no
110
- | `off`, `basic`, `full`
111
- | `full`
112
- | The health-check mode.
111
+ === Environment variables
112
+
113
+ [cols="1,1,1,1,2",options="header"]
114
+ |===
115
+ | Name | Required | Type | Default | Meaning
113
116
 
114
117
  | `PI_CODING_AGENT_DIR`
115
118
  | no
@@ -132,12 +135,11 @@ The command:
132
135
  - discovers Requesty models (and optional health checks)
133
136
  - shows a summary notification (added/removed IDs, health summary)
134
137
  - asks for confirmation before writing `~/.pi/agent/models.json`
138
+ - refreshes Pi's model registry so the new models are immediately available in `/model`
135
139
  - reports whether the file was updated
136
140
 
137
141
  If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
138
142
 
139
- After a successful write, run `/reload`.
140
-
141
143
  [source,text]
142
144
  ----
143
145
  /requesty-discover --dry-run
@@ -145,22 +147,31 @@ After a successful write, run `/reload`.
145
147
 
146
148
  Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
147
149
 
150
+ [source,bash]
151
+ ----
152
+ pi -p "/requesty-discover"
153
+ pi -p "/requesty-discover --dry-run"
154
+ ----
155
+
156
+ You can also run the command from the CLI in a non-interactive mode.
157
+
148
158
  == Usage status
149
159
 
150
160
  The extension shows your current Requesty monthly spend as a persistent line in Pi's footer, e.g.:
151
161
 
152
162
  [source,text]
153
163
  ----
154
- Requesty Usage (<API Key Name>): $63.55/$150.00 (42%)
164
+ <API Key Name>: $63.55/$150.00 (42%)
155
165
  ----
156
166
 
157
167
  When your API key has no monthly limit (`monthly_limit` is `0`), the limit is treated as unlimited:
158
168
 
159
169
  [source,text]
160
170
  ----
161
- Requesty Usage (<API Key Name>): $63.55 (unlimited)
171
+ <API Key Name>: $63.55 (unlimited)
162
172
  ----
163
173
 
164
- When you select a model from a provider other than `REQUESTY_PROVIDER_ID`, the status is cleared — there is no Requesty usage to show for it. If `REQUESTY_API_KEY` is not configured, the status is left untouched.
174
+ When you select a model from a provider other than `REQUESTY_PROVIDER_ID`, the status is cleared — there is no Requesty usage to show for it.
165
175
 
166
- NOTE: The footer update is best-effort and never blocks a turn. If it fails (network error, stale context after `/reload`), it fails silently without spamming notifications.
176
+ NOTE: The footer update is best-effort and never blocks a turn.
177
+ If it fails (network error, stale context after `/reload`), it fails silently without spamming notifications.
package/README.md CHANGED
@@ -6,11 +6,13 @@ This extension helps you discover currently available models in requesty.ai for
6
6
 
7
7
  This is a fork of [pi-requesty](https://github.com/requestyai/pi-requesty) with additional features:
8
8
 
9
- * Use `REQUESTY_API_KEY` from the environment
9
+ * Use the `apiKey` from the model registry allowing you to use
10
+ [pi’s full value resolution](https://pi.dev/docs/latest/models#value-resolution) capabilities
10
11
  * Health-check available models and write a log about them
11
12
  * Brief information about what was found, including added/removed models, health summary
12
13
  * Confirm before writing `models.json`, with a dry-run mode that only previews
13
14
  * A footer status line showing your monthly Requesty spend against your monthly limit
15
+ * Optional Settings file in **json5** format
14
16
 
15
17
  ## When and why use it
16
18
 
@@ -48,7 +50,7 @@ pi -e ./pi-requesty-discover
48
50
  ## Configuration
49
51
 
50
52
  The extension reads the `requesty-export` provider from `~/.pi/agent/models.json`.
51
- You can override the provider id with the `REQUESTY_PROVIDER_ID` variable.
53
+ You can override the provider id via the settings.
52
54
 
53
55
  Example:
54
56
 
@@ -58,8 +60,8 @@ Example:
58
60
  "requesty-export": {
59
61
  "name": "Requesty",
60
62
  "baseUrl": "https://router.requesty.ai/v1",
61
- "apiKey": "$REQUESTY_API_KEY",
62
63
  "api": "openai-completions",
64
+ "apiKey": "$REQUESTY_API_KEY",
63
65
  "models": []
64
66
  }
65
67
  }
@@ -67,23 +69,37 @@ Example:
67
69
  ```
68
70
 
69
71
  **❗ IMPORTANT**\
70
- The `apiKey` from `models.json` is not used by this extension.
71
- It may contain Pi-specific environment-variable substitution that the extension cannot reliably resolve.
72
- Set `REQUESTY_API_KEY` instead. This variable is required.
73
- You can configure the real `apiKey` as you like, but it would be recommended to use the same variable as for the model discovery.
72
+ Health checks are designed to use only a few tokens, but they still make real Requesty API calls and ***will*** consume credits.
74
73
 
75
- **❗ IMPORTANT**\
76
- Health checks are designed to use only a few tokens, but they still make real Requesty API calls and may consume credits.
74
+ On startup, the extension fetches `<baseUrl>/models` using the configured `apiKey` as the bearer token and registers discovered and healthy models with Pi.
75
+
76
+ ### Discovery settings
77
+
78
+ The discovery workflow reads optional settings from `requesty-discovery-settings.json5` in the Pi config directory (`PI_CODING_AGENT_DIR`).
79
+ The file uses JSON5, so it supports comments. It is optional; missing settings default to empty.
80
+
81
+ Example:
82
+
83
+ ```json5
84
+ {
85
+ // override the default provider id
86
+ providerId: "requesty-export",
87
+ // configure the health check mode
88
+ healthCheckMode: "full", // or "basic", "off"
89
+ // models excluded from discovery; useful for models that keep failing health checks
90
+ bannedModels: [
91
+ // hangs on tool calls
92
+ 'requesty/unstable-model',
93
+ ],
94
+ }
95
+ ```
77
96
 
78
- On startup, the extension fetches `<baseUrl>/models` using `REQUESTY_API_KEY` as the bearer token and registers discovered and healthy models with Pi.
97
+ Banned models are excluded before health checks run (so they cost no credits) and are removed from `models.json` on the next discovery write.
79
98
 
80
99
  ### Environment variables
81
100
 
82
101
  | Name | Required | Type | Default | Meaning |
83
102
  | --- | --- | --- | --- | --- |
84
- | `REQUESTY_API_KEY` | yes | `string` | | Requesty API key used for model discovery and health checks. |
85
- | `REQUESTY_PROVIDER_ID` | no | `string` | `requesty-export` | The id of the provider. |
86
- | `REQUESTY_HEALTH_CHECK_MODE` | no | `off`, `basic`, `full` | `full` | The health-check mode. |
87
103
  | `PI_CODING_AGENT_DIR` | no | `string` | `~/.pi/agent` | pi configuration directory |
88
104
 
89
105
  ## Command
@@ -99,33 +115,40 @@ The command:
99
115
  * discovers Requesty models (and optional health checks)
100
116
  * shows a summary notification (added/removed IDs, health summary)
101
117
  * asks for confirmation before writing `~/.pi/agent/models.json`
118
+ * refreshes Pi’s model registry so the new models are immediately available in `/model`
102
119
  * reports whether the file was updated
103
120
 
104
121
  If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
105
122
 
106
- After a successful write, run `/reload`.
107
-
108
123
  ```text
109
124
  /requesty-discover --dry-run
110
125
  ```
111
126
 
112
127
  Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
113
128
 
129
+ ```bash
130
+ pi -p "/requesty-discover"
131
+ pi -p "/requesty-discover --dry-run"
132
+ ```
133
+
134
+ You can also run the command from the CLI in a non-interactive mode.
135
+
114
136
  ## Usage status
115
137
 
116
138
  The extension shows your current Requesty monthly spend as a persistent line in Pi’s footer, e.g.:
117
139
 
118
140
  ```text
119
- Requesty Usage (<API Key Name>): $63.55/$150.00 (42%)
141
+ <API Key Name>: $63.55/$150.00 (42%)
120
142
  ```
121
143
 
122
144
  When your API key has no monthly limit (`monthly_limit` is `0`), the limit is treated as unlimited:
123
145
 
124
146
  ```text
125
- Requesty Usage (<API Key Name>): $63.55 (unlimited)
147
+ <API Key Name>: $63.55 (unlimited)
126
148
  ```
127
149
 
128
- When you select a model from a provider other than `REQUESTY_PROVIDER_ID`, the status is cleared — there is no Requesty usage to show for it. If `REQUESTY_API_KEY` is not configured, the status is left untouched.
150
+ When you select a model from a provider other than `REQUESTY_PROVIDER_ID`, the status is cleared — there is no Requesty usage to show for it.
129
151
 
130
152
  **📌 NOTE**\
131
- The footer update is best-effort and never blocks a turn. If it fails (network error, stale context after `/reload`), it fails silently without spamming notifications.
153
+ The footer update is best-effort and never blocks a turn.
154
+ If it fails (network error, stale context after `/reload`), it fails silently without spamming notifications.
@@ -4,49 +4,43 @@ Short notes for non-obvious decisions: context, decision, why not the obvious al
4
4
 
5
5
  == Usage status stays on `setStatus` (shared, truncated footer line)
6
6
 
7
- Pi's `FooterComponent` joins all extensions' `setStatus` entries on one line and
8
- ellipsis-truncates the joined line to terminal width. No per-key layout, no wrap.
7
+ Pi's `FooterComponent` joins all extensions' `setStatus` entries on one line and ellipsis-truncates the joined line to terminal width.
8
+ No per-key layout, no wrap.
9
9
 
10
- `ctx.ui.setFooter(factory)` is replacement-only — it swaps the whole footer (pwd,
11
- tokens, context %, model, statuses), and the factory gets `footerData` but not
12
- token/model state. No "extra footer" API exists.
10
+ `ctx.ui.setFooter(factory)` is replacement-only — it swaps the whole footer (pwd, tokens, context %, model, statuses), and the factory gets `footerData` but not token/model state.
11
+ No "extra footer" API exists.
13
12
 
14
- Decision: keep `setStatus`. Revisit when the line overflows visibly with other
15
- extensions' statuses, or if the usage string grows past a typical width.
13
+ Decision: keep `setStatus`.
14
+ Revisit when the line overflows visibly with other extensions' statuses, or if the usage string grows past a typical width.
16
15
 
17
16
  == Hand-rolled `Try<T>` instead of an effects library
18
17
 
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.
18
+ Context: env loading and discovery evaluation both need a caught-error result value (`{ ok: true, value } | { ok: false, error }`).
19
+ Libraries like Effect ship an industrial `Either`/`Effect.try` for exactly this.
22
20
 
23
21
  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.
22
+ Two call sites in a Pi extension don't justify the dependency weight or the learning curve for contributors.
23
+ The hand-rolled version is ~10 lines (`Try<T>`, `runCatching`, `runCatchingAsync`, named after Kotlin's `Result`), lives in `utils.ts`, and documents intent just as well.
28
24
 
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.
25
+ Decision: keep the hand-rolled `Try<T>`.
26
+ Revisit if the extension ever needs typed errors, retries, or resource scoping; that's the day Effect earns its place.
32
27
 
33
28
  == Not a provider — a models.json-writing extension
34
29
 
35
- Context: Requesty already works as a Pi provider via `models.json`. The temptation
36
- is to make this extension *be* the provider: read the model list from the Requesty
37
- API, hold it in memory, surface usage stats, let Pi manage the API key.
30
+ Context: Requesty already works as a Pi provider via `models.json`.
31
+ The temptation is to make this extension *be* the provider: read the model list from the Requesty API, hold it in memory, surface usage stats, let Pi manage the API key.
38
32
 
39
33
  Why not:
40
34
 
41
- - Health-checking is the core value and has no home in a provider. A provider serves
42
- models; it doesn't vet them. The health check plus the diff/confirm/write flow is a
43
- discovery-and-selection step, not serving.
44
- - A provider keeps its model list in-memory and refreshes occasionally. Here the
45
- list is a persistent on-disk snapshot (`models.json`) — constant until you re-run
46
- `/requesty-discover`. That matches the intent: an explicit, reviewed, user-confirmed write,
47
- after which Pi's normal provider machinery serves the models.
35
+ - Health-checking is the core value and has no home in a provider.
36
+ A provider serves models; it doesn't vet them.
37
+ The health check plus the diff/confirm/write flow is a discovery-and-selection step, not serving.
38
+ - A provider keeps its model list in-memory and refreshes occasionally.
39
+ Here the list is a persistent on-disk snapshot (`models.json`) — constant until you re-run
40
+ `/requesty-discover`.
41
+ That matches the intent: an explicit, reviewed, user-confirmed write, after which Pi's normal provider machinery serves the models.
48
42
 
49
- The provider-like parts (API model fetch, usage footer, wanting Pi-managed auth) are
50
- real, but the health check and the snapshot model break the provider fit.
43
+ The provider-like parts (API model fetch, usage footer, wanting Pi-managed auth) are real, but the health check and the snapshot model break the provider fit.
51
44
 
52
45
  Decision: stay a `/requesty-discover` command that writes `models.json`.
46
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-requesty-discover",
3
- "version": "2.1.1",
3
+ "version": "3.0.1",
4
4
  "description": "Discover available Requesty models for the Pi Coding Agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,7 +47,8 @@
47
47
  ]
48
48
  },
49
49
  "devDependencies": {
50
- "@earendil-works/pi-tui": "^0.80.3",
50
+ "@earendil-works/pi-tui": "^0.85.0",
51
+ "@earendil-works/pi-coding-agent": "^0.85.0",
51
52
  "@eslint/js": "^10.0.1",
52
53
  "@types/node": "^26.1.0",
53
54
  "@vitest/coverage-v8": "^4.1.9",
@@ -63,6 +64,7 @@
63
64
  "vitest": "^4.1.9"
64
65
  },
65
66
  "dependencies": {
67
+ "json5": "^2.2.3",
66
68
  "zod": "^4.4.3"
67
69
  }
68
70
  }
@@ -0,0 +1,222 @@
1
+ import type { ProviderModelConfig } from '@earendil-works/pi-coding-agent'
2
+ import { type Env } from './env'
3
+ import {
4
+ type GetApiKey,
5
+ type ModelsDiff,
6
+ type ModelsJson,
7
+ diffModels,
8
+ formatModelsDiffSummary,
9
+ getRequestyConfig,
10
+ updateModelsJson,
11
+ } from './models-json'
12
+ import { checkModels, formatHealthSummary, writeHealthCheckLog } from './health-check'
13
+ import { discoverModels } from './requesty-api'
14
+ import type { DiscoverySettings } from './settings'
15
+ import { formatErrorMessage, runCatchingAsync } from './utils'
16
+
17
+ const DRY_RUN_ARG = '--dry-run'
18
+
19
+ interface AutocompleteItem {
20
+ value: string
21
+ label: string
22
+ description: string
23
+ }
24
+
25
+ export type NotificationLevel = 'info' | 'warning' | 'error'
26
+
27
+ /** Everything the discovery workflow needs from the UI. One port, two adapters (tui/console) in index.ts. */
28
+ export type DiscoveryUi = {
29
+ notify(message: string, level: NotificationLevel): void
30
+ confirm(title: string, message: string): Promise<boolean>
31
+ setStatus(message: string): void
32
+ }
33
+
34
+ export type RefreshModelsRegistry = () => Promise<void>
35
+
36
+ export type DiscoveryEvaluation = {
37
+ dryRun: boolean
38
+ modelCount: number
39
+ failedCount: number
40
+ warningCount: number
41
+ passing: ProviderModelConfig[]
42
+ diff: ModelsDiff
43
+ healthCheckSummary: string
44
+ logNote: string
45
+ data: ModelsJson
46
+ }
47
+
48
+ export function formatDiscoveryFailure(error: unknown): string {
49
+ const detail = formatErrorMessage(error)
50
+ return `Discovery failed: ${detail}`
51
+ }
52
+
53
+ export async function evaluateDiscovery(
54
+ args: string,
55
+ settings: DiscoverySettings,
56
+ env: Env,
57
+ ui: DiscoveryUi,
58
+ getApiKey: GetApiKey,
59
+ ): Promise<DiscoveryEvaluation> {
60
+ ui.setStatus('Discovering Requesty models...')
61
+ const dryRun = args.split(' ').includes(DRY_RUN_ARG)
62
+
63
+ const { data, provider, existingModelIds } = await getRequestyConfig(getApiKey, settings, env)
64
+ const bannedModels = new Set(settings.bannedModels)
65
+ const allModels = await discoverModels(provider)
66
+ const foundBannedModels = allModels
67
+ .filter(model => bannedModels.has(model.id))
68
+ .map(model => model.id)
69
+ .toSorted()
70
+ const models = allModels.filter(model => !bannedModels.has(model.id))
71
+ const modelsMap = new Map(models.map(m => [m.id, m]))
72
+
73
+ let diff: ModelsDiff
74
+ let failedCount = 0
75
+ let warningCount = 0
76
+ let passing: ProviderModelConfig[] = []
77
+ let logNote = ''
78
+ let healthCheckSummary = ''
79
+
80
+ if (settings.healthCheckMode !== 'off') {
81
+ if (models.length > 0) {
82
+ ui.setStatus(`Checking models 0/${models.length}...`)
83
+ const healthResults = await checkModels(provider, models, settings.healthCheckMode === 'full', {
84
+ onProgress: ({ completed, total }) => {
85
+ ui.setStatus(`Checking models ${completed}/${total}...`)
86
+ },
87
+ })
88
+ const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
89
+ failedCount = sortedResults.filter(r => r.status === 'error').length
90
+ warningCount = sortedResults.filter(r => r.status === 'warning').length
91
+ passing = sortedResults.flatMap(r => {
92
+ const model = modelsMap.get(r.modelId)
93
+ if (!model) return []
94
+ if (r.status === 'ok') return [model]
95
+ // warnings are transient: assume that the error will go away and add them anyway
96
+ if (r.status === 'warning') return [model]
97
+ return []
98
+ })
99
+ healthCheckSummary = formatHealthSummary(sortedResults)
100
+ writeHealthCheckLog(
101
+ provider,
102
+ sortedResults,
103
+ diffModels(existingModelIds, passing),
104
+ { providerId: settings.providerId, bannedModels: foundBannedModels },
105
+ env,
106
+ )
107
+ }
108
+ diff = diffModels(existingModelIds, passing)
109
+ logNote = `Full health check log: ${env.health_check_log_path}\n`
110
+ } else {
111
+ passing = models
112
+ diff = diffModels(existingModelIds, passing)
113
+ }
114
+
115
+ return {
116
+ dryRun,
117
+ modelCount: models.length,
118
+ failedCount,
119
+ warningCount,
120
+ passing,
121
+ diff,
122
+ healthCheckSummary,
123
+ logNote,
124
+ data,
125
+ }
126
+ }
127
+
128
+ export async function finalizeDiscovery(
129
+ evaluation: DiscoveryEvaluation,
130
+ settings: DiscoverySettings,
131
+ env: Env,
132
+ ui: DiscoveryUi,
133
+ refresh?: RefreshModelsRegistry,
134
+ ): Promise<void> {
135
+ const level = notificationLevel(evaluation)
136
+ const summary = buildDiscoverySummary(evaluation)
137
+
138
+ // Always surface the discovery result first (toast styling), then decide.
139
+ if (evaluation.dryRun) {
140
+ ui.notify(
141
+ `${summary}
142
+ Dry run: left models.json unchanged.`,
143
+ level,
144
+ )
145
+ return
146
+ }
147
+
148
+ if (evaluation.passing.length === 0) {
149
+ ui.notify(
150
+ `${summary}
151
+ Left models.json unchanged.`,
152
+ level,
153
+ )
154
+ return
155
+ }
156
+
157
+ ui.notify(summary, level)
158
+ const { title, message } = buildConfirmPrompt(evaluation)
159
+ const shouldUpdate = await ui.confirm(title, message)
160
+ if (shouldUpdate) {
161
+ updateModelsJson(evaluation.data, evaluation.passing, settings, env)
162
+ if (!refresh) {
163
+ ui.notify('Updated models.json.', 'info')
164
+ return
165
+ }
166
+ const refreshResult = await runCatchingAsync(() => refresh())
167
+ if (refreshResult.ok) {
168
+ ui.notify('Updated models.json. New models are available in /model.', 'info')
169
+ } else {
170
+ ui.notify(
171
+ `Updated models.json, but the model registry could not be refreshed: ${formatErrorMessage(refreshResult.error)}. Run /reload or restart Pi to use the changes.`,
172
+ 'warning',
173
+ )
174
+ }
175
+ return
176
+ }
177
+
178
+ ui.notify('Left models.json unchanged.', 'info')
179
+ }
180
+
181
+ function buildDiscoverySummary(evaluation: DiscoveryEvaluation): string {
182
+ return [
183
+ `Discovered ${evaluation.modelCount} Requesty model(s).`,
184
+ evaluation.healthCheckSummary.trimEnd(),
185
+ formatModelsDiffSummary(evaluation.diff),
186
+ evaluation.logNote.trimEnd(),
187
+ ]
188
+ .filter(part => part.length > 0)
189
+ .join('\n')
190
+ }
191
+
192
+ function buildConfirmPrompt(evaluation: DiscoveryEvaluation): { title: string; message: string } {
193
+ const hasIdChanges = evaluation.diff.added.length > 0 || evaluation.diff.removed.length > 0
194
+ if (hasIdChanges) {
195
+ return {
196
+ title: `Write ${evaluation.passing.length} model(s)?`,
197
+ message: 'Update models.json with the discovery result above.',
198
+ }
199
+ }
200
+ return {
201
+ title: 'Refresh models.json?',
202
+ message: 'No model ID changes. Rewrite the file to refresh metadata anyway?',
203
+ }
204
+ }
205
+
206
+ function notificationLevel(evaluation: DiscoveryEvaluation): NotificationLevel {
207
+ if (evaluation.failedCount > 0) return 'error'
208
+ if (evaluation.warningCount > 0) return 'warning'
209
+ return 'info'
210
+ }
211
+
212
+ export function getArgumentCompletions(prefix: string): AutocompleteItem[] {
213
+ const options = [
214
+ {
215
+ value: DRY_RUN_ARG,
216
+ label: DRY_RUN_ARG,
217
+ description: 'Preview discovery without offering to write models.json',
218
+ },
219
+ ]
220
+ if (!prefix) return options
221
+ return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
222
+ }
package/src/env.ts CHANGED
@@ -1,37 +1,17 @@
1
1
  import path from 'node:path'
2
- import process from 'node:process'
3
- import { prettifyError, z } from 'zod'
4
2
  import { getAgentDir } from '@earendil-works/pi-coding-agent'
5
3
 
6
- const HealthCheckModeSchema = z.enum(['off', 'basic', 'full']).default('full')
7
-
8
- export const DEFAULT_PROVIDER_ID = 'requesty-export'
9
-
10
4
  export type Env = {
11
5
  models_json_path: string
12
6
  health_check_log_path: string
13
- provider_id: string
14
- requesty_api_key: string
15
- health_check_mode: z.infer<typeof HealthCheckModeSchema>
7
+ settings_path: string
16
8
  }
17
9
 
18
10
  export function getEnv(): Env {
19
- const envVars = process.env
20
- const result = HealthCheckModeSchema.safeParse(envVars.REQUESTY_HEALTH_CHECK_MODE)
21
- if (!result.success) {
22
- throw new Error(prettifyError(result.error))
23
- }
24
-
25
- const apiKey = envVars.REQUESTY_API_KEY
26
- if (!apiKey) {
27
- throw new Error(`apiKey must be set via REQUESTY_API_KEY env var`)
28
- }
29
11
  const agentPath = getAgentDir()
30
12
  return {
31
13
  models_json_path: path.join(agentPath, 'models.json'),
32
14
  health_check_log_path: path.join(agentPath, 'requesty-health-check.log'),
33
- provider_id: envVars.REQUESTY_PROVIDER_ID ?? DEFAULT_PROVIDER_ID,
34
- requesty_api_key: apiKey,
35
- health_check_mode: result.data,
15
+ settings_path: path.join(agentPath, 'requesty-discovery-settings.json5'),
36
16
  }
37
17
  }