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 +46 -35
- package/README.md +42 -19
- package/docs/DECISIONS.adoc +23 -29
- package/package.json +4 -2
- package/src/discovery.ts +222 -0
- package/src/env.ts +2 -22
- package/src/health-check.ts +63 -34
- package/src/index.ts +106 -261
- package/src/models-json.ts +32 -11
- package/src/settings.ts +47 -0
- package/src/ui/requesty-status-loader.ts +1 -1
- package/src/utils.ts +24 -0
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,
|
|
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 `
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
=== Discovery settings
|
|
89
88
|
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|===
|
|
94
|
-
| Name | Required | Type | Default | Meaning
|
|
92
|
+
Example:
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
package/docs/DECISIONS.adoc
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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`.
|
|
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
|
-
|
|
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
|
-
|
|
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>`.
|
|
30
|
-
|
|
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`.
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- A provider keeps its model list in-memory and refreshes occasionally.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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": "
|
|
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.
|
|
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
|
}
|
package/src/discovery.ts
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|