pi-requesty-discover 2.1.1 → 2.2.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 +21 -23
- package/README.md +18 -17
- package/docs/DECISIONS.adoc +2 -1
- package/package.json +3 -2
- package/src/discovery.ts +224 -0
- package/src/env.ts +0 -6
- package/src/index.ts +85 -253
- package/src/models-json.ts +18 -7
package/README.adoc
CHANGED
|
@@ -8,13 +8,14 @@ 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
|
|
@@ -70,35 +71,24 @@ Example:
|
|
|
70
71
|
"requesty-export": {
|
|
71
72
|
"name": "Requesty",
|
|
72
73
|
"baseUrl": "https://router.requesty.ai/v1",
|
|
73
|
-
"apiKey": "$REQUESTY_API_KEY",
|
|
74
74
|
"api": "openai-completions",
|
|
75
|
+
"apiKey": "$REQUESTY_API_KEY",
|
|
75
76
|
"models": []
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
----
|
|
80
81
|
|
|
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.
|
|
85
|
-
|
|
86
|
-
IMPORTANT: Health checks are designed to use only a few tokens, but they still make real Requesty API calls and may consume credits.
|
|
82
|
+
IMPORTANT: Health checks are designed to use only a few tokens, but they still make real Requesty API calls and **will** consume credits.
|
|
87
83
|
|
|
88
|
-
On startup, the extension fetches `<baseUrl>/models` using `
|
|
84
|
+
On startup, the extension fetches `<baseUrl>/models` using the configured `apiKey` as the bearer token and registers discovered and healthy models with Pi.
|
|
89
85
|
|
|
90
86
|
=== Environment variables
|
|
91
87
|
|
|
92
|
-
[cols="1,1,1,1,2",
|
|
88
|
+
[cols="1,1,1,1,2",options="header"]
|
|
93
89
|
|===
|
|
94
90
|
| Name | Required | Type | Default | Meaning
|
|
95
91
|
|
|
96
|
-
| `REQUESTY_API_KEY`
|
|
97
|
-
| yes
|
|
98
|
-
| `string`
|
|
99
|
-
|
|
|
100
|
-
| Requesty API key used for model discovery and health checks.
|
|
101
|
-
|
|
102
92
|
| `REQUESTY_PROVIDER_ID`
|
|
103
93
|
| no
|
|
104
94
|
| `string`
|
|
@@ -132,12 +122,11 @@ The command:
|
|
|
132
122
|
- discovers Requesty models (and optional health checks)
|
|
133
123
|
- shows a summary notification (added/removed IDs, health summary)
|
|
134
124
|
- asks for confirmation before writing `~/.pi/agent/models.json`
|
|
125
|
+
- refreshes Pi's model registry so the new models are immediately available in `/model`
|
|
135
126
|
- reports whether the file was updated
|
|
136
127
|
|
|
137
128
|
If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
|
|
138
129
|
|
|
139
|
-
After a successful write, run `/reload`.
|
|
140
|
-
|
|
141
130
|
[source,text]
|
|
142
131
|
----
|
|
143
132
|
/requesty-discover --dry-run
|
|
@@ -145,22 +134,31 @@ After a successful write, run `/reload`.
|
|
|
145
134
|
|
|
146
135
|
Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
|
|
147
136
|
|
|
137
|
+
[source,bash]
|
|
138
|
+
----
|
|
139
|
+
pi -p "/requesty-discover"
|
|
140
|
+
pi -p "/requesty-discover --dry-run"
|
|
141
|
+
----
|
|
142
|
+
|
|
143
|
+
You can also run the command from the CLI in a non-interactive mode.
|
|
144
|
+
|
|
148
145
|
== Usage status
|
|
149
146
|
|
|
150
147
|
The extension shows your current Requesty monthly spend as a persistent line in Pi's footer, e.g.:
|
|
151
148
|
|
|
152
149
|
[source,text]
|
|
153
150
|
----
|
|
154
|
-
|
|
151
|
+
<API Key Name>: $63.55/$150.00 (42%)
|
|
155
152
|
----
|
|
156
153
|
|
|
157
154
|
When your API key has no monthly limit (`monthly_limit` is `0`), the limit is treated as unlimited:
|
|
158
155
|
|
|
159
156
|
[source,text]
|
|
160
157
|
----
|
|
161
|
-
|
|
158
|
+
<API Key Name>: $63.55 (unlimited)
|
|
162
159
|
----
|
|
163
160
|
|
|
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.
|
|
161
|
+
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
162
|
|
|
166
|
-
NOTE: The footer update is best-effort and never blocks a turn.
|
|
163
|
+
NOTE: The footer update is best-effort and never blocks a turn.
|
|
164
|
+
If it fails (network error, stale context after `/reload`), it fails silently without spamming notifications.
|
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ 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
|
|
@@ -58,8 +59,8 @@ Example:
|
|
|
58
59
|
"requesty-export": {
|
|
59
60
|
"name": "Requesty",
|
|
60
61
|
"baseUrl": "https://router.requesty.ai/v1",
|
|
61
|
-
"apiKey": "$REQUESTY_API_KEY",
|
|
62
62
|
"api": "openai-completions",
|
|
63
|
+
"apiKey": "$REQUESTY_API_KEY",
|
|
63
64
|
"models": []
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -67,21 +68,14 @@ Example:
|
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
**❗ 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.
|
|
71
|
+
Health checks are designed to use only a few tokens, but they still make real Requesty API calls and ***will*** consume credits.
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
Health checks are designed to use only a few tokens, but they still make real Requesty API calls and may consume credits.
|
|
77
|
-
|
|
78
|
-
On startup, the extension fetches `<baseUrl>/models` using `REQUESTY_API_KEY` as the bearer token and registers discovered and healthy models with Pi.
|
|
73
|
+
On startup, the extension fetches `<baseUrl>/models` using the configured `apiKey` as the bearer token and registers discovered and healthy models with Pi.
|
|
79
74
|
|
|
80
75
|
### Environment variables
|
|
81
76
|
|
|
82
77
|
| Name | Required | Type | Default | Meaning |
|
|
83
78
|
| --- | --- | --- | --- | --- |
|
|
84
|
-
| `REQUESTY_API_KEY` | yes | `string` | | Requesty API key used for model discovery and health checks. |
|
|
85
79
|
| `REQUESTY_PROVIDER_ID` | no | `string` | `requesty-export` | The id of the provider. |
|
|
86
80
|
| `REQUESTY_HEALTH_CHECK_MODE` | no | `off`, `basic`, `full` | `full` | The health-check mode. |
|
|
87
81
|
| `PI_CODING_AGENT_DIR` | no | `string` | `~/.pi/agent` | pi configuration directory |
|
|
@@ -99,33 +93,40 @@ The command:
|
|
|
99
93
|
* discovers Requesty models (and optional health checks)
|
|
100
94
|
* shows a summary notification (added/removed IDs, health summary)
|
|
101
95
|
* asks for confirmation before writing `~/.pi/agent/models.json`
|
|
96
|
+
* refreshes Pi’s model registry so the new models are immediately available in `/model`
|
|
102
97
|
* reports whether the file was updated
|
|
103
98
|
|
|
104
99
|
If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
|
|
105
100
|
|
|
106
|
-
After a successful write, run `/reload`.
|
|
107
|
-
|
|
108
101
|
```text
|
|
109
102
|
/requesty-discover --dry-run
|
|
110
103
|
```
|
|
111
104
|
|
|
112
105
|
Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
|
|
113
106
|
|
|
107
|
+
```bash
|
|
108
|
+
pi -p "/requesty-discover"
|
|
109
|
+
pi -p "/requesty-discover --dry-run"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
You can also run the command from the CLI in a non-interactive mode.
|
|
113
|
+
|
|
114
114
|
## Usage status
|
|
115
115
|
|
|
116
116
|
The extension shows your current Requesty monthly spend as a persistent line in Pi’s footer, e.g.:
|
|
117
117
|
|
|
118
118
|
```text
|
|
119
|
-
|
|
119
|
+
<API Key Name>: $63.55/$150.00 (42%)
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
When your API key has no monthly limit (`monthly_limit` is `0`), the limit is treated as unlimited:
|
|
123
123
|
|
|
124
124
|
```text
|
|
125
|
-
|
|
125
|
+
<API Key Name>: $63.55 (unlimited)
|
|
126
126
|
```
|
|
127
127
|
|
|
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.
|
|
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.
|
|
129
129
|
|
|
130
130
|
**📌 NOTE**\
|
|
131
|
-
The footer update is best-effort and never blocks a turn.
|
|
131
|
+
The footer update is best-effort and never blocks a turn.
|
|
132
|
+
If it fails (network error, stale context after `/reload`), it fails silently without spamming notifications.
|
package/docs/DECISIONS.adoc
CHANGED
|
@@ -24,7 +24,7 @@ Why not: Effect is a runtime, a generator DSL, and a worldview — not a utility
|
|
|
24
24
|
Two call sites in a Pi extension don't justify the dependency weight or the
|
|
25
25
|
learning curve for contributors. The hand-rolled version is ~10 lines
|
|
26
26
|
(`Try<T>`, `runCatching`, `runCatchingAsync`, named after Kotlin's `Result`),
|
|
27
|
-
lives in `src/
|
|
27
|
+
lives in `src/discovery.ts`, and documents intent just as well.
|
|
28
28
|
|
|
29
29
|
Decision: keep the hand-rolled `Try<T>`. No `monads.ts` module either — rule of
|
|
30
30
|
three. Revisit if the extension ever needs typed errors, retries, or resource
|
|
@@ -50,3 +50,4 @@ The provider-like parts (API model fetch, usage footer, wanting Pi-managed auth)
|
|
|
50
50
|
real, but the health check and the snapshot model break the provider fit.
|
|
51
51
|
|
|
52
52
|
Decision: stay a `/requesty-discover` command that writes `models.json`.
|
|
53
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-requesty-discover",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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",
|
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { ProviderModelConfig } from '@earendil-works/pi-coding-agent'
|
|
2
|
+
import { type Env } from './env'
|
|
3
|
+
import {
|
|
4
|
+
diffModels,
|
|
5
|
+
formatModelsDiffSummary,
|
|
6
|
+
GetApiKey,
|
|
7
|
+
getRequestyConfig,
|
|
8
|
+
type ModelsDiff,
|
|
9
|
+
ModelsJson,
|
|
10
|
+
updateModelsJson,
|
|
11
|
+
} from './models-json'
|
|
12
|
+
import { checkModels, formatHealthSummary, writeHealthCheckLog } from './health-check'
|
|
13
|
+
import { discoverModels } from './requesty-api'
|
|
14
|
+
|
|
15
|
+
const DRY_RUN_ARG = '--dry-run'
|
|
16
|
+
|
|
17
|
+
interface AutocompleteItem {
|
|
18
|
+
value: string
|
|
19
|
+
label: string
|
|
20
|
+
description: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type NotificationLevel = 'info' | 'warning' | 'error'
|
|
24
|
+
|
|
25
|
+
/** Everything the discovery workflow needs from the UI. One port, two adapters (tui/console) in index.ts. */
|
|
26
|
+
export type DiscoveryUi = {
|
|
27
|
+
notify(message: string, level: NotificationLevel): void
|
|
28
|
+
confirm(title: string, message: string): Promise<boolean>
|
|
29
|
+
setStatus(message: string): void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type RefreshModelsRegistry = () => Promise<void>
|
|
33
|
+
|
|
34
|
+
export type DiscoveryEvaluation = {
|
|
35
|
+
dryRun: boolean
|
|
36
|
+
modelCount: number
|
|
37
|
+
failedCount: number
|
|
38
|
+
passing: ProviderModelConfig[]
|
|
39
|
+
diff: ModelsDiff
|
|
40
|
+
healthCheckSummary: string
|
|
41
|
+
logNote: string
|
|
42
|
+
data: ModelsJson
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type Try<T> = { ok: true; value: T } | { ok: false; error: unknown }
|
|
46
|
+
|
|
47
|
+
export function formatDiscoveryFailure(error: unknown): string {
|
|
48
|
+
const detail = formatError(error)
|
|
49
|
+
return `Discovery failed: ${detail}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function evaluateDiscovery(
|
|
53
|
+
args: string,
|
|
54
|
+
env: Env,
|
|
55
|
+
ui: DiscoveryUi,
|
|
56
|
+
getApiKey: GetApiKey,
|
|
57
|
+
): Promise<DiscoveryEvaluation> {
|
|
58
|
+
ui.setStatus('Discovering Requesty models...')
|
|
59
|
+
const dryRun = args.split(' ').includes(DRY_RUN_ARG)
|
|
60
|
+
|
|
61
|
+
const { data, provider, existingModelIds } = await getRequestyConfig(getApiKey, env)
|
|
62
|
+
const models = await discoverModels(provider)
|
|
63
|
+
const modelsMap = new Map(models.map(m => [m.id, m]))
|
|
64
|
+
|
|
65
|
+
let diff: ModelsDiff
|
|
66
|
+
let failedCount = 0
|
|
67
|
+
let passing: ProviderModelConfig[]
|
|
68
|
+
let logNote = ''
|
|
69
|
+
let healthCheckSummary = ''
|
|
70
|
+
|
|
71
|
+
if (env.health_check_mode !== 'off') {
|
|
72
|
+
ui.setStatus(`Checking models 0/${models.length}...`)
|
|
73
|
+
const healthResults = await checkModels(provider, models, env.health_check_mode === 'full', {
|
|
74
|
+
onProgress: ({ completed, total }) => {
|
|
75
|
+
ui.setStatus(`Checking models ${completed}/${total}...`)
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
|
|
79
|
+
failedCount = sortedResults.filter(r => !r.ok).length
|
|
80
|
+
passing = sortedResults.flatMap(r => {
|
|
81
|
+
const model = modelsMap.get(r.modelId)
|
|
82
|
+
return r.ok && model ? [model] : []
|
|
83
|
+
})
|
|
84
|
+
diff = diffModels(existingModelIds, passing)
|
|
85
|
+
healthCheckSummary = formatHealthSummary(sortedResults)
|
|
86
|
+
writeHealthCheckLog(provider, sortedResults, diff, env)
|
|
87
|
+
logNote = `Full health check log: ${env.health_check_log_path}\n`
|
|
88
|
+
} else {
|
|
89
|
+
passing = models
|
|
90
|
+
diff = diffModels(existingModelIds, passing)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
dryRun,
|
|
95
|
+
modelCount: models.length,
|
|
96
|
+
failedCount,
|
|
97
|
+
passing,
|
|
98
|
+
diff,
|
|
99
|
+
healthCheckSummary,
|
|
100
|
+
logNote,
|
|
101
|
+
data,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function finalizeDiscovery(
|
|
106
|
+
evaluation: DiscoveryEvaluation,
|
|
107
|
+
env: Env,
|
|
108
|
+
ui: DiscoveryUi,
|
|
109
|
+
refresh?: RefreshModelsRegistry,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const level = notificationLevel(evaluation)
|
|
112
|
+
const summary = buildDiscoverySummary(evaluation)
|
|
113
|
+
|
|
114
|
+
// Always surface the discovery result first (toast styling), then decide.
|
|
115
|
+
if (evaluation.dryRun) {
|
|
116
|
+
ui.notify(
|
|
117
|
+
`${summary}
|
|
118
|
+
Dry run: left models.json unchanged.`,
|
|
119
|
+
level,
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (evaluation.passing.length === 0) {
|
|
125
|
+
ui.notify(
|
|
126
|
+
`${summary}
|
|
127
|
+
Left models.json unchanged.`,
|
|
128
|
+
level,
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ui.notify(summary, level)
|
|
134
|
+
const { title, message } = buildConfirmPrompt(evaluation)
|
|
135
|
+
const shouldUpdate = await ui.confirm(title, message)
|
|
136
|
+
if (shouldUpdate) {
|
|
137
|
+
updateModelsJson(evaluation.data, evaluation.passing, env)
|
|
138
|
+
if (!refresh) {
|
|
139
|
+
ui.notify('Updated models.json.', 'info')
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
const refreshResult = await runCatchingAsync(() => refresh())
|
|
143
|
+
if (refreshResult.ok) {
|
|
144
|
+
ui.notify('Updated models.json. New models are available in /model.', 'info')
|
|
145
|
+
} else {
|
|
146
|
+
ui.notify(
|
|
147
|
+
`Updated models.json, but the model registry could not be refreshed: ${formatError(refreshResult.error)}. Run /reload or restart Pi to use the changes.`,
|
|
148
|
+
'warning',
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
ui.notify('Left models.json unchanged.', 'info')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildDiscoverySummary(evaluation: DiscoveryEvaluation): string {
|
|
158
|
+
return [
|
|
159
|
+
`Discovered ${evaluation.modelCount} Requesty model(s).`,
|
|
160
|
+
evaluation.healthCheckSummary.trimEnd(),
|
|
161
|
+
formatModelsDiffSummary(evaluation.diff),
|
|
162
|
+
evaluation.logNote.trimEnd(),
|
|
163
|
+
]
|
|
164
|
+
.filter(part => part.length > 0)
|
|
165
|
+
.join('\n')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildConfirmPrompt(evaluation: DiscoveryEvaluation): { title: string; message: string } {
|
|
169
|
+
const hasIdChanges = evaluation.diff.added.length > 0 || evaluation.diff.removed.length > 0
|
|
170
|
+
if (hasIdChanges) {
|
|
171
|
+
return {
|
|
172
|
+
title: `Write ${evaluation.passing.length} model(s)?`,
|
|
173
|
+
message: 'Update models.json with the discovery result above.',
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
title: 'Refresh models.json?',
|
|
178
|
+
message: 'No model ID changes. Rewrite the file to refresh metadata anyway?',
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function notificationLevel(evaluation: DiscoveryEvaluation): NotificationLevel {
|
|
183
|
+
if (evaluation.failedCount === 0) return 'info'
|
|
184
|
+
if (evaluation.failedCount < evaluation.modelCount) return 'warning'
|
|
185
|
+
return 'error'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function getArgumentCompletions(prefix: string): AutocompleteItem[] {
|
|
189
|
+
const options = [
|
|
190
|
+
{
|
|
191
|
+
value: DRY_RUN_ARG,
|
|
192
|
+
label: DRY_RUN_ARG,
|
|
193
|
+
description: 'Preview discovery without offering to write models.json',
|
|
194
|
+
},
|
|
195
|
+
]
|
|
196
|
+
if (!prefix) return options
|
|
197
|
+
return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function formatError(error: unknown) {
|
|
201
|
+
return error instanceof Error ? error.message : String(error)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function runCatching<T>(fn: () => T): Try<T> {
|
|
205
|
+
try {
|
|
206
|
+
return { ok: true, value: fn() }
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return { ok: false, error }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function runCatchingAsync<T>(fn: () => Promise<T>): Promise<Try<T>> {
|
|
213
|
+
try {
|
|
214
|
+
return { ok: true, value: await fn() }
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return { ok: false, error }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function complainOnBrokenEnv(ui: DiscoveryUi, env: Try<Env>) {
|
|
221
|
+
if (!env.ok) {
|
|
222
|
+
ui.notify(`failed to load env: ${formatError(env.error)}`, 'error')
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/env.ts
CHANGED
|
@@ -11,7 +11,6 @@ export type Env = {
|
|
|
11
11
|
models_json_path: string
|
|
12
12
|
health_check_log_path: string
|
|
13
13
|
provider_id: string
|
|
14
|
-
requesty_api_key: string
|
|
15
14
|
health_check_mode: z.infer<typeof HealthCheckModeSchema>
|
|
16
15
|
}
|
|
17
16
|
|
|
@@ -22,16 +21,11 @@ export function getEnv(): Env {
|
|
|
22
21
|
throw new Error(prettifyError(result.error))
|
|
23
22
|
}
|
|
24
23
|
|
|
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
24
|
const agentPath = getAgentDir()
|
|
30
25
|
return {
|
|
31
26
|
models_json_path: path.join(agentPath, 'models.json'),
|
|
32
27
|
health_check_log_path: path.join(agentPath, 'requesty-health-check.log'),
|
|
33
28
|
provider_id: envVars.REQUESTY_PROVIDER_ID ?? DEFAULT_PROVIDER_ID,
|
|
34
|
-
requesty_api_key: apiKey,
|
|
35
29
|
health_check_mode: result.data,
|
|
36
30
|
}
|
|
37
31
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,60 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionCommandContext,
|
|
4
|
-
ExtensionContext,
|
|
5
|
-
ProviderModelConfig,
|
|
6
|
-
} from '@earendil-works/pi-coding-agent'
|
|
1
|
+
import { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from '@earendil-works/pi-coding-agent'
|
|
7
2
|
import { type Env, getEnv } from './env'
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
formatModelsDiffSummary,
|
|
11
|
-
getRequestyConfig,
|
|
12
|
-
type ModelsDiff,
|
|
13
|
-
updateModelsJson,
|
|
14
|
-
} from './models-json'
|
|
15
|
-
import { type ApiKeyInfo, discoverModels, fetchApiUsage } from './requesty-api'
|
|
16
|
-
import { checkModels, formatHealthSummary, writeHealthCheckLog } from './health-check'
|
|
3
|
+
import { GetApiKey, getRequestyConfig } from './models-json'
|
|
4
|
+
import { type ApiKeyInfo, fetchApiUsage } from './requesty-api'
|
|
17
5
|
import { RequestyStatusLoader } from './ui/requesty-status-loader.ts'
|
|
6
|
+
import {
|
|
7
|
+
complainOnBrokenEnv,
|
|
8
|
+
type DiscoveryEvaluation,
|
|
9
|
+
type DiscoveryUi,
|
|
10
|
+
evaluateDiscovery,
|
|
11
|
+
finalizeDiscovery,
|
|
12
|
+
formatDiscoveryFailure,
|
|
13
|
+
getArgumentCompletions,
|
|
14
|
+
RefreshModelsRegistry,
|
|
15
|
+
runCatching,
|
|
16
|
+
runCatchingAsync,
|
|
17
|
+
type Try,
|
|
18
|
+
} from './discovery'
|
|
19
|
+
|
|
20
|
+
export { type Try }
|
|
18
21
|
|
|
19
22
|
const COMMAND_NAME = 'requesty-discover'
|
|
20
|
-
const DRY_RUN_ARG = '--dry-run'
|
|
21
23
|
export const USAGE_STATUS_KEY = 'requesty-usage'
|
|
22
24
|
|
|
23
|
-
interface AutocompleteItem {
|
|
24
|
-
value: string
|
|
25
|
-
label: string
|
|
26
|
-
description: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type NotificationLevel = 'info' | 'warning' | 'error'
|
|
30
|
-
|
|
31
|
-
export type Notifier = {
|
|
32
|
-
notify(message: string, level: NotificationLevel): void
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type Confirmer = {
|
|
36
|
-
confirm(title: string, message: string): Promise<boolean>
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type StatusReporter = {
|
|
40
|
-
set(message: string): void
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
type ModelsJsonData = ReturnType<typeof getRequestyConfig>['data']
|
|
44
|
-
|
|
45
|
-
type DiscoveryEvaluation = {
|
|
46
|
-
dryRun: boolean
|
|
47
|
-
modelCount: number
|
|
48
|
-
failedCount: number
|
|
49
|
-
passing: ProviderModelConfig[]
|
|
50
|
-
diff: ModelsDiff
|
|
51
|
-
healthCheckSummary: string
|
|
52
|
-
logNote: string
|
|
53
|
-
data: ModelsJsonData
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type Try<T> = { ok: true; value: T } | { ok: false; error: unknown }
|
|
57
|
-
|
|
58
25
|
// token suppresses stale writes when turns overlap
|
|
59
26
|
let latestToken: object = {}
|
|
60
27
|
|
|
@@ -75,7 +42,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
75
42
|
})
|
|
76
43
|
|
|
77
44
|
pi.on('session_start', (_event, ctx) => {
|
|
78
|
-
|
|
45
|
+
const ui = createTuiUi(ctx)
|
|
46
|
+
complainOnBrokenEnv(ui, env)
|
|
79
47
|
void updateUsageStatus(ctx, env)
|
|
80
48
|
})
|
|
81
49
|
|
|
@@ -85,210 +53,64 @@ export default function (pi: ExtensionAPI) {
|
|
|
85
53
|
}
|
|
86
54
|
|
|
87
55
|
export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, env: Try<Env>, args: string) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
56
|
+
const ui = ctx.mode === 'tui' ? createTuiUi(ctx) : createConsoleUi()
|
|
57
|
+
complainOnBrokenEnv(ui, env)
|
|
58
|
+
if (!env.ok) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
91
62
|
if (ctx.mode !== 'tui') {
|
|
63
|
+
return runSilentDiscoveryWorkflow(ctx, env.value, args, ui)
|
|
64
|
+
} else {
|
|
65
|
+
return runInteractiveDiscoverWorkflow(ctx, env.value, args, ui)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runSilentDiscoveryWorkflow(
|
|
70
|
+
ctx: ExtensionCommandContext,
|
|
71
|
+
env: Env,
|
|
72
|
+
args: string,
|
|
73
|
+
ui: DiscoveryUi,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const evaluationResult = await runCatchingAsync(() => evaluateDiscovery(args, env, ui, createGetApiKey(ctx)))
|
|
76
|
+
if (!evaluationResult.ok) {
|
|
77
|
+
ui.notify(formatDiscoveryFailure(evaluationResult.error), 'error')
|
|
92
78
|
return
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
await finalizeDiscovery(evaluationResult.value, env, ui)
|
|
82
|
+
}
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
// Errors are wrapped because ctx.ui.custom resolves via done() and does not reject.
|
|
84
|
+
async function runInteractiveDiscoverWorkflow(ctx: ExtensionCommandContext, env: Env, args: string, ui: DiscoveryUi) {
|
|
100
85
|
const evaluationResult: Try<DiscoveryEvaluation> = await runWithStatusUi(
|
|
101
86
|
ctx,
|
|
102
87
|
'Discovering models...',
|
|
103
|
-
async
|
|
88
|
+
async statusUi => await runCatchingAsync(() => evaluateDiscovery(args, env, statusUi, createGetApiKey(ctx))),
|
|
104
89
|
)
|
|
105
90
|
|
|
106
91
|
if (!evaluationResult.ok) {
|
|
107
|
-
|
|
92
|
+
ui.notify(formatDiscoveryFailure(evaluationResult.error), 'error')
|
|
108
93
|
return
|
|
109
94
|
}
|
|
110
95
|
|
|
111
|
-
|
|
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}`
|
|
96
|
+
await finalizeDiscovery(evaluationResult.value, env, ui, createRefreshRegistry(ctx))
|
|
118
97
|
}
|
|
119
98
|
|
|
120
99
|
async function runWithStatusUi<T>(
|
|
121
100
|
ctx: ExtensionCommandContext,
|
|
122
101
|
initialMessage: string,
|
|
123
|
-
fn: (
|
|
102
|
+
fn: (statusUi: DiscoveryUi) => Promise<T>,
|
|
124
103
|
): Promise<T> {
|
|
125
|
-
return ctx.ui.custom<T>((
|
|
126
|
-
const loader = new RequestyStatusLoader(
|
|
127
|
-
const status = createLoaderStatusReporter(loader)
|
|
104
|
+
return ctx.ui.custom<T>((tui, theme, _kb, done) => {
|
|
105
|
+
const loader = new RequestyStatusLoader(tui, theme, initialMessage)
|
|
128
106
|
void Promise.resolve()
|
|
129
|
-
.then(() => fn(
|
|
107
|
+
.then(() => fn(createTuiUi(ctx, loader)))
|
|
130
108
|
.then(done)
|
|
131
109
|
.catch(done)
|
|
132
110
|
return loader
|
|
133
111
|
})
|
|
134
112
|
}
|
|
135
113
|
|
|
136
|
-
async function evaluateDiscovery(args: string, env: Env, status: StatusReporter): Promise<DiscoveryEvaluation> {
|
|
137
|
-
status.set('Discovering Requesty models...')
|
|
138
|
-
const dryRun = args.split(' ').includes(DRY_RUN_ARG)
|
|
139
|
-
|
|
140
|
-
const { data, provider, existingModelIds } = getRequestyConfig(env)
|
|
141
|
-
const models = await discoverModels(provider)
|
|
142
|
-
const modelsMap = new Map(models.map(m => [m.id, m]))
|
|
143
|
-
|
|
144
|
-
let diff: ModelsDiff
|
|
145
|
-
let failedCount = 0
|
|
146
|
-
let passing: ProviderModelConfig[]
|
|
147
|
-
let logNote = ''
|
|
148
|
-
let healthCheckSummary = ''
|
|
149
|
-
|
|
150
|
-
if (env.health_check_mode !== 'off') {
|
|
151
|
-
status.set(`Checking models 0/${models.length}...`)
|
|
152
|
-
const healthResults = await checkModels(provider, models, env.health_check_mode === 'full', {
|
|
153
|
-
onProgress: ({ completed, total }) => {
|
|
154
|
-
status.set(`Checking models ${completed}/${total}...`)
|
|
155
|
-
},
|
|
156
|
-
})
|
|
157
|
-
const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
|
|
158
|
-
failedCount = sortedResults.filter(r => !r.ok).length
|
|
159
|
-
passing = sortedResults.flatMap(r => {
|
|
160
|
-
const model = modelsMap.get(r.modelId)
|
|
161
|
-
return r.ok && model ? [model] : []
|
|
162
|
-
})
|
|
163
|
-
diff = diffModels(existingModelIds, passing)
|
|
164
|
-
healthCheckSummary = formatHealthSummary(sortedResults)
|
|
165
|
-
writeHealthCheckLog(provider, sortedResults, diff, env)
|
|
166
|
-
logNote = `Full health check log: ${env.health_check_log_path}\n`
|
|
167
|
-
} else {
|
|
168
|
-
passing = models
|
|
169
|
-
diff = diffModels(existingModelIds, passing)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
dryRun,
|
|
174
|
-
modelCount: models.length,
|
|
175
|
-
failedCount,
|
|
176
|
-
passing,
|
|
177
|
-
diff,
|
|
178
|
-
healthCheckSummary,
|
|
179
|
-
logNote,
|
|
180
|
-
data,
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function finalizeDiscovery(
|
|
185
|
-
evaluation: DiscoveryEvaluation,
|
|
186
|
-
confirmer: Confirmer,
|
|
187
|
-
notifier: Notifier,
|
|
188
|
-
env: Env,
|
|
189
|
-
): Promise<void> {
|
|
190
|
-
const level = notificationLevel(evaluation)
|
|
191
|
-
const summary = buildDiscoverySummary(evaluation)
|
|
192
|
-
|
|
193
|
-
// Always surface the discovery result first (toast styling), then decide.
|
|
194
|
-
if (evaluation.dryRun) {
|
|
195
|
-
notifier.notify(
|
|
196
|
-
`${summary}
|
|
197
|
-
Dry run: left models.json unchanged.`,
|
|
198
|
-
level,
|
|
199
|
-
)
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (evaluation.passing.length === 0) {
|
|
204
|
-
notifier.notify(
|
|
205
|
-
`${summary}
|
|
206
|
-
Left models.json unchanged.`,
|
|
207
|
-
level,
|
|
208
|
-
)
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
notifier.notify(summary, level)
|
|
213
|
-
const { title, message } = buildConfirmPrompt(evaluation)
|
|
214
|
-
const shouldUpdate = await confirmer.confirm(title, message)
|
|
215
|
-
if (shouldUpdate) {
|
|
216
|
-
updateModelsJson(evaluation.data, evaluation.passing, env)
|
|
217
|
-
notifier.notify('Updated models.json. Run /reload to use the changes.', 'info')
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
notifier.notify('Left models.json unchanged.', 'info')
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function buildDiscoverySummary(evaluation: DiscoveryEvaluation): string {
|
|
225
|
-
return [
|
|
226
|
-
`Discovered ${evaluation.modelCount} Requesty model(s).`,
|
|
227
|
-
evaluation.healthCheckSummary.trimEnd(),
|
|
228
|
-
formatModelsDiffSummary(evaluation.diff),
|
|
229
|
-
evaluation.logNote.trimEnd(),
|
|
230
|
-
]
|
|
231
|
-
.filter(part => part.length > 0)
|
|
232
|
-
.join('\n')
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function buildConfirmPrompt(evaluation: DiscoveryEvaluation): { title: string; message: string } {
|
|
236
|
-
const hasIdChanges = evaluation.diff.added.length > 0 || evaluation.diff.removed.length > 0
|
|
237
|
-
if (hasIdChanges) {
|
|
238
|
-
return {
|
|
239
|
-
title: `Write ${evaluation.passing.length} model(s)?`,
|
|
240
|
-
message: 'Update models.json with the discovery result above.',
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return {
|
|
244
|
-
title: 'Refresh models.json?',
|
|
245
|
-
message: 'No model ID changes. Rewrite the file to refresh metadata anyway?',
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function notificationLevel(evaluation: DiscoveryEvaluation): NotificationLevel {
|
|
250
|
-
if (evaluation.failedCount === 0) return 'info'
|
|
251
|
-
if (evaluation.failedCount < evaluation.modelCount) return 'warning'
|
|
252
|
-
return 'error'
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function getArgumentCompletions(prefix: string): AutocompleteItem[] {
|
|
256
|
-
const options = [
|
|
257
|
-
{
|
|
258
|
-
value: DRY_RUN_ARG,
|
|
259
|
-
label: DRY_RUN_ARG,
|
|
260
|
-
description: 'Preview discovery without offering to write models.json',
|
|
261
|
-
},
|
|
262
|
-
]
|
|
263
|
-
if (!prefix) return options
|
|
264
|
-
return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function createUiNotifier(ctx: ExtensionContext): Notifier {
|
|
268
|
-
return {
|
|
269
|
-
notify(message: string, level?: NotificationLevel) {
|
|
270
|
-
const prefixedMessage = `${COMMAND_NAME}: ${message}`
|
|
271
|
-
ctx.ui.notify(prefixedMessage, level)
|
|
272
|
-
},
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function createUiConfirmer(ctx: ExtensionContext): Confirmer {
|
|
277
|
-
return {
|
|
278
|
-
confirm(title, message) {
|
|
279
|
-
return ctx.ui.confirm(title, message)
|
|
280
|
-
},
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function createLoaderStatusReporter(loader: RequestyStatusLoader): StatusReporter {
|
|
285
|
-
return {
|
|
286
|
-
set(message: string) {
|
|
287
|
-
loader.setMessage(message)
|
|
288
|
-
},
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
114
|
async function updateUsageStatus(ctx: ExtensionContext, env: Try<Env>): Promise<void> {
|
|
293
115
|
if (!env.ok) return
|
|
294
116
|
if (!ctx.hasUI) return // no footer to write to (print/json mode): skip the wasted fetch
|
|
@@ -300,7 +122,7 @@ async function updateUsageStatus(ctx: ExtensionContext, env: Try<Env>): Promise<
|
|
|
300
122
|
ctx.ui.setStatus(USAGE_STATUS_KEY, undefined)
|
|
301
123
|
return
|
|
302
124
|
}
|
|
303
|
-
const info = await fetchUsageStatus(env.value)
|
|
125
|
+
const info = await fetchUsageStatus(createGetApiKey(ctx), env.value)
|
|
304
126
|
if (latestToken !== token) return
|
|
305
127
|
ctx.ui.setStatus(USAGE_STATUS_KEY, formatUsageStatus(info))
|
|
306
128
|
} catch {
|
|
@@ -311,30 +133,26 @@ async function updateUsageStatus(ctx: ExtensionContext, env: Try<Env>): Promise<
|
|
|
311
133
|
export function formatUsageStatus(info: ApiKeyInfo): string {
|
|
312
134
|
const spend = `$${info.monthlySpend.toFixed(2)}`
|
|
313
135
|
if (info.monthlyLimit <= 0) {
|
|
314
|
-
return
|
|
136
|
+
return `${info.name}: ${spend} (unlimited)`
|
|
315
137
|
}
|
|
316
138
|
const limit = `$${info.monthlyLimit.toFixed(2)}`
|
|
317
139
|
const percent = Math.floor((info.monthlySpend / info.monthlyLimit) * 100)
|
|
318
|
-
return
|
|
140
|
+
return `${info.name}: ${spend}/${limit} (${percent}%)`
|
|
319
141
|
}
|
|
320
142
|
|
|
321
143
|
let lastFetched: { value: ApiKeyInfo; time: Date } | undefined
|
|
322
144
|
|
|
323
|
-
async function fetchUsageStatus(env: Env): Promise<ApiKeyInfo> {
|
|
145
|
+
async function fetchUsageStatus(getApiKey: GetApiKey, env: Env): Promise<ApiKeyInfo> {
|
|
324
146
|
const now = new Date()
|
|
325
147
|
if (lastFetched?.time && now.getTime() - lastFetched.time.getTime() < 2000) {
|
|
326
148
|
return lastFetched.value
|
|
327
149
|
}
|
|
328
|
-
const { provider } = getRequestyConfig(env)
|
|
150
|
+
const { provider } = await getRequestyConfig(getApiKey, env)
|
|
329
151
|
const value = await fetchApiUsage(provider)
|
|
330
152
|
lastFetched = { value, time: new Date() }
|
|
331
153
|
return value
|
|
332
154
|
}
|
|
333
155
|
|
|
334
|
-
function formatError(error: unknown) {
|
|
335
|
-
return error instanceof Error ? error.message : String(error)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
156
|
/**
|
|
339
157
|
* For testing purposes, reset the cache after each run
|
|
340
158
|
*/
|
|
@@ -342,25 +160,39 @@ export function resetUsageStatusCache(): void {
|
|
|
342
160
|
lastFetched = undefined
|
|
343
161
|
}
|
|
344
162
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
163
|
+
/** TUI adapter: routes the workflow's UI needs to Pi's ctx.ui. */
|
|
164
|
+
export function createTuiUi(ctx: ExtensionContext, loader?: RequestyStatusLoader): DiscoveryUi {
|
|
165
|
+
return {
|
|
166
|
+
notify: (message, level) => ctx.ui.notify(`${COMMAND_NAME}: ${message}`, level),
|
|
167
|
+
confirm: (title, message) => ctx.ui.confirm(title, message),
|
|
168
|
+
setStatus: message => loader?.setMessage(message),
|
|
350
169
|
}
|
|
351
170
|
}
|
|
352
171
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Console adapter for non-interactive modes. Always confirms: print mode is
|
|
174
|
+
* non-interactive, so the write proceeds without a confirmation dialog.
|
|
175
|
+
*/
|
|
176
|
+
export function createConsoleUi(): DiscoveryUi {
|
|
177
|
+
return {
|
|
178
|
+
notify: (message, level) => console.log(`[${level}] ${message}`),
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- must match the DiscoveryUi promise signature
|
|
180
|
+
confirm: async () => true,
|
|
181
|
+
setStatus: message => console.log(message),
|
|
358
182
|
}
|
|
359
183
|
}
|
|
360
184
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
185
|
+
/** Re-reads models.json through Pi's model registry so new models are usable without /reload. */
|
|
186
|
+
export function createRefreshRegistry(ctx: ExtensionContext): RefreshModelsRegistry {
|
|
187
|
+
return () =>
|
|
188
|
+
ctx.modelRegistry.refresh({ allowNetwork: false }).then(result => {
|
|
189
|
+
if (result.aborted || result.errors.size > 0) {
|
|
190
|
+
const detail = [...result.errors.entries()].map(([id, error]) => `${id}: ${error.message}`).join(', ')
|
|
191
|
+
throw new Error(detail || 'refresh aborted')
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createGetApiKey(ctx: ExtensionContext): GetApiKey {
|
|
197
|
+
return providerId => ctx.modelRegistry.getApiKeyForProvider(providerId)
|
|
366
198
|
}
|
package/src/models-json.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ProviderConfig, ProviderModelConfig } from '@earendil-works/pi-coding-agent'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { z } from 'zod'
|
|
5
|
-
import {
|
|
5
|
+
import { type Env, getEnv } from './env'
|
|
6
6
|
|
|
7
7
|
const DEFAULT_BASE_URL = 'https://router.requesty.ai/v1'
|
|
8
8
|
const DEFAULT_NAME = 'Requesty'
|
|
9
9
|
|
|
10
|
+
/** Resolves the API key for a provider through Pi's model registry. */
|
|
11
|
+
export type GetApiKey = (providerId: string) => Promise<string | undefined>
|
|
12
|
+
|
|
10
13
|
const ProviderSchema = z
|
|
11
14
|
.object({
|
|
12
15
|
name: z.string().optional(),
|
|
13
16
|
baseUrl: z.string().optional(),
|
|
14
17
|
apiKey: z.string().optional(),
|
|
18
|
+
api: z.string().optional(),
|
|
15
19
|
models: z.array(z.object({ id: z.string() }).catchall(z.unknown())).optional(),
|
|
16
20
|
})
|
|
17
21
|
.catchall(z.unknown())
|
|
@@ -22,7 +26,7 @@ const ModelsJsonSchema = z
|
|
|
22
26
|
})
|
|
23
27
|
.catchall(z.unknown())
|
|
24
28
|
|
|
25
|
-
type ModelsJson = z.infer<typeof ModelsJsonSchema>
|
|
29
|
+
export type ModelsJson = z.infer<typeof ModelsJsonSchema>
|
|
26
30
|
type ModelsJsonProvider = z.infer<typeof ProviderSchema>
|
|
27
31
|
|
|
28
32
|
export type RequestyProvider = ModelsJsonProvider & {
|
|
@@ -42,7 +46,7 @@ export type ModelsDiff = {
|
|
|
42
46
|
removed: string[]
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
export function getRequestyConfig(envConfig: Env = getEnv()): RequestyConfig {
|
|
49
|
+
export async function getRequestyConfig(getApiKey: GetApiKey, envConfig: Env = getEnv()): Promise<RequestyConfig> {
|
|
46
50
|
const data = readModelsJson(envConfig)
|
|
47
51
|
const provider = data.providers[envConfig.provider_id]
|
|
48
52
|
|
|
@@ -50,7 +54,10 @@ export function getRequestyConfig(envConfig: Env = getEnv()): RequestyConfig {
|
|
|
50
54
|
throw new Error(`${envConfig.models_json_path} does not define providers.${envConfig.provider_id}`)
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
const apiKey = envConfig.
|
|
57
|
+
const apiKey = await getApiKey(envConfig.provider_id)
|
|
58
|
+
if (!apiKey) {
|
|
59
|
+
throw new Error(`No API key found for provider ${envConfig.provider_id}`)
|
|
60
|
+
}
|
|
54
61
|
return {
|
|
55
62
|
data,
|
|
56
63
|
existingModelIds: (provider.models ?? []).map(m => m.id),
|
|
@@ -83,9 +90,13 @@ export function formatModelsDiffSummary(diff: ModelsDiff): string {
|
|
|
83
90
|
|
|
84
91
|
export function updateModelsJson(data: ModelsJson, models: ProviderModelConfig[], envConfig: Env = getEnv()): void {
|
|
85
92
|
const provider = data.providers[envConfig.provider_id]
|
|
93
|
+
const { name, baseUrl, api, apiKey, models: _existingModels, ...passthrough } = provider
|
|
86
94
|
data.providers[envConfig.provider_id] = {
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
name,
|
|
96
|
+
baseUrl,
|
|
97
|
+
api,
|
|
98
|
+
apiKey,
|
|
99
|
+
...passthrough,
|
|
89
100
|
models: models.map(model => ({
|
|
90
101
|
id: model.id,
|
|
91
102
|
name: model.name,
|