pi-requesty-discover 1.2.0 → 2.1.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/CONTRIBUTING.adoc +1 -0
- package/README.adoc +36 -5
- package/README.md +35 -5
- package/docs/DECISIONS.adoc +36 -0
- package/package.json +1 -1
- package/src/env.ts +3 -1
- package/src/index.ts +250 -89
- package/src/requesty-api.ts +29 -0
package/CONTRIBUTING.adoc
CHANGED
|
@@ -32,4 +32,5 @@ Notes:
|
|
|
32
32
|
* Keep implementation in `src/`; root `index.ts` is just the package entrypoint.
|
|
33
33
|
* Edit `README.adoc` if necessary.
|
|
34
34
|
* See `AGENTS.md` for detailed repository conventions.
|
|
35
|
+
* See `docs/DECISIONS.adoc` for deliberate design decisions.
|
|
35
36
|
* I am pretty good at identifying AI slop 😁.
|
package/README.adoc
CHANGED
|
@@ -16,7 +16,9 @@ This is a fork of https://github.com/requestyai/pi-requesty[pi-requesty] with ad
|
|
|
16
16
|
|
|
17
17
|
* Use `REQUESTY_API_KEY` from the environment
|
|
18
18
|
* Health-check available models and write a log about them
|
|
19
|
-
*
|
|
19
|
+
* Brief information about what was found, including added/removed models, health summary
|
|
20
|
+
* Confirm before writing `models.json`, with a dry-run mode that only previews
|
|
21
|
+
* A footer status line showing your monthly Requesty spend against your monthly limit
|
|
20
22
|
|
|
21
23
|
== When and why use it
|
|
22
24
|
|
|
@@ -25,7 +27,8 @@ Use this extension if
|
|
|
25
27
|
- you are on Requesty.ai
|
|
26
28
|
- you are limited to certain models by a company policy (for instance, for data residency/protection reasons)
|
|
27
29
|
- you don't want to handcraft your own curated list of models constantly
|
|
28
|
-
- you want to health
|
|
30
|
+
- you want to health-check models and preview the diff before writing
|
|
31
|
+
- you want to keep in touch with your usage without having to constantly visit the Requesty dashboard
|
|
29
32
|
|
|
30
33
|
== Installation
|
|
31
34
|
|
|
@@ -124,12 +127,40 @@ Inside Pi:
|
|
|
124
127
|
/requesty-discover
|
|
125
128
|
----
|
|
126
129
|
|
|
127
|
-
The command
|
|
128
|
-
|
|
130
|
+
The command:
|
|
131
|
+
|
|
132
|
+
- discovers Requesty models (and optional health checks)
|
|
133
|
+
- shows a summary notification (added/removed IDs, health summary)
|
|
134
|
+
- asks for confirmation before writing `~/.pi/agent/models.json`
|
|
135
|
+
- reports whether the file was updated
|
|
136
|
+
|
|
137
|
+
If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
|
|
138
|
+
|
|
139
|
+
After a successful write, run `/reload`.
|
|
129
140
|
|
|
130
141
|
[source,text]
|
|
131
142
|
----
|
|
132
143
|
/requesty-discover --dry-run
|
|
133
144
|
----
|
|
134
145
|
|
|
135
|
-
|
|
146
|
+
Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
|
|
147
|
+
|
|
148
|
+
== Usage status
|
|
149
|
+
|
|
150
|
+
The extension shows your current Requesty monthly spend as a persistent line in Pi's footer, e.g.:
|
|
151
|
+
|
|
152
|
+
[source,text]
|
|
153
|
+
----
|
|
154
|
+
Requesty Usage (<API Key Name>): $63.55/$150.00 (42%)
|
|
155
|
+
----
|
|
156
|
+
|
|
157
|
+
When your API key has no monthly limit (`monthly_limit` is `0`), the limit is treated as unlimited:
|
|
158
|
+
|
|
159
|
+
[source,text]
|
|
160
|
+
----
|
|
161
|
+
Requesty Usage (<API Key Name>): $63.55 (unlimited)
|
|
162
|
+
----
|
|
163
|
+
|
|
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.
|
|
165
|
+
|
|
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.
|
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@ This is a fork of [pi-requesty](https://github.com/requestyai/pi-requesty) with
|
|
|
8
8
|
|
|
9
9
|
* Use `REQUESTY_API_KEY` from the environment
|
|
10
10
|
* Health-check available models and write a log about them
|
|
11
|
-
*
|
|
11
|
+
* Brief information about what was found, including added/removed models, health summary
|
|
12
|
+
* Confirm before writing `models.json`, with a dry-run mode that only previews
|
|
13
|
+
* A footer status line showing your monthly Requesty spend against your monthly limit
|
|
12
14
|
|
|
13
15
|
## When and why use it
|
|
14
16
|
|
|
@@ -17,7 +19,8 @@ Use this extension if
|
|
|
17
19
|
* you are on Requesty.ai
|
|
18
20
|
* you are limited to certain models by a company policy (for instance, for data residency/protection reasons)
|
|
19
21
|
* you don’t want to handcraft your own curated list of models constantly
|
|
20
|
-
* you want to health
|
|
22
|
+
* you want to health-check models and preview the diff before writing
|
|
23
|
+
* you want to keep in touch with your usage without having to constantly visit the Requesty dashboard
|
|
21
24
|
|
|
22
25
|
## Installation
|
|
23
26
|
|
|
@@ -91,11 +94,38 @@ Inside Pi:
|
|
|
91
94
|
/requesty-discover
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
The command
|
|
95
|
-
|
|
97
|
+
The command:
|
|
98
|
+
|
|
99
|
+
* discovers Requesty models (and optional health checks)
|
|
100
|
+
* shows a summary notification (added/removed IDs, health summary)
|
|
101
|
+
* asks for confirmation before writing `~/.pi/agent/models.json`
|
|
102
|
+
* reports whether the file was updated
|
|
103
|
+
|
|
104
|
+
If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
|
|
105
|
+
|
|
106
|
+
After a successful write, run `/reload`.
|
|
96
107
|
|
|
97
108
|
```text
|
|
98
109
|
/requesty-discover --dry-run
|
|
99
110
|
```
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+
Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
|
|
113
|
+
|
|
114
|
+
## Usage status
|
|
115
|
+
|
|
116
|
+
The extension shows your current Requesty monthly spend as a persistent line in Pi’s footer, e.g.:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
Requesty Usage (<API Key Name>): $63.55/$150.00 (42%)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When your API key has no monthly limit (`monthly_limit` is `0`), the limit is treated as unlimited:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
Requesty Usage (<API Key Name>): $63.55 (unlimited)
|
|
126
|
+
```
|
|
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. If `REQUESTY_API_KEY` is not configured, the status is left untouched.
|
|
129
|
+
|
|
130
|
+
**📌 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.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
= Decision records
|
|
2
|
+
|
|
3
|
+
Short notes for non-obvious decisions: context, decision, why not the obvious alternative.
|
|
4
|
+
|
|
5
|
+
== Usage status stays on `setStatus` (shared, truncated footer line)
|
|
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.
|
|
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.
|
|
13
|
+
|
|
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.
|
|
16
|
+
|
|
17
|
+
== Not a provider — a models.json-writing extension
|
|
18
|
+
|
|
19
|
+
Context: Requesty already works as a Pi provider via `models.json`. The temptation
|
|
20
|
+
is to make this extension *be* the provider: read the model list from the Requesty
|
|
21
|
+
API, hold it in memory, surface usage stats, let Pi manage the API key.
|
|
22
|
+
|
|
23
|
+
Why not:
|
|
24
|
+
|
|
25
|
+
- Health-checking is the core value and has no home in a provider. A provider serves
|
|
26
|
+
models; it doesn't vet them. The health check plus the diff/confirm/write flow is a
|
|
27
|
+
discovery-and-selection step, not serving.
|
|
28
|
+
- A provider keeps its model list in-memory and refreshes occasionally. Here the
|
|
29
|
+
list is a persistent on-disk snapshot (`models.json`) — constant until you re-run
|
|
30
|
+
`/requesty-discover`. That matches the intent: an explicit, reviewed, user-confirmed write,
|
|
31
|
+
after which Pi's normal provider machinery serves the models.
|
|
32
|
+
|
|
33
|
+
The provider-like parts (API model fetch, usage footer, wanting Pi-managed auth) are
|
|
34
|
+
real, but the health check and the snapshot model break the provider fit.
|
|
35
|
+
|
|
36
|
+
Decision: stay a `/requesty-discover` command that writes `models.json`.
|
package/package.json
CHANGED
package/src/env.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { getAgentDir } from '@earendil-works/pi-coding-agent'
|
|
|
5
5
|
|
|
6
6
|
const HealthCheckModeSchema = z.enum(['off', 'basic', 'full']).default('full')
|
|
7
7
|
|
|
8
|
+
export const DEFAULT_PROVIDER_ID = 'requesty-export'
|
|
9
|
+
|
|
8
10
|
export type Env = {
|
|
9
11
|
models_json_path: string
|
|
10
12
|
health_check_log_path: string
|
|
@@ -28,7 +30,7 @@ export function getEnv(): Env {
|
|
|
28
30
|
return {
|
|
29
31
|
models_json_path: path.join(agentPath, 'models.json'),
|
|
30
32
|
health_check_log_path: path.join(agentPath, 'requesty-health-check.log'),
|
|
31
|
-
provider_id: envVars.REQUESTY_PROVIDER_ID ??
|
|
33
|
+
provider_id: envVars.REQUESTY_PROVIDER_ID ?? DEFAULT_PROVIDER_ID,
|
|
32
34
|
requesty_api_key: apiKey,
|
|
33
35
|
health_check_mode: result.data,
|
|
34
36
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
ProviderModelConfig,
|
|
6
|
+
} from '@earendil-works/pi-coding-agent'
|
|
7
|
+
import { type Env, getEnv } from './env'
|
|
3
8
|
import {
|
|
4
9
|
diffModels,
|
|
5
10
|
formatModelsDiffSummary,
|
|
6
11
|
getRequestyConfig,
|
|
7
|
-
updateModelsJson,
|
|
8
12
|
type ModelsDiff,
|
|
13
|
+
updateModelsJson,
|
|
9
14
|
} from './models-json'
|
|
10
|
-
import { discoverModels } from './requesty-api'
|
|
15
|
+
import { type ApiKeyInfo, discoverModels, fetchApiUsage } from './requesty-api'
|
|
11
16
|
import { checkModels, formatHealthSummary, writeHealthCheckLog } from './health-check'
|
|
12
17
|
import { RequestyStatusLoader } from './ui/requesty-status-loader.ts'
|
|
13
18
|
|
|
14
19
|
const COMMAND_NAME = 'requesty-discover'
|
|
15
20
|
const DRY_RUN_ARG = '--dry-run'
|
|
21
|
+
export const USAGE_STATUS_KEY = 'requesty-usage'
|
|
16
22
|
|
|
17
23
|
interface AutocompleteItem {
|
|
18
24
|
value: string
|
|
@@ -26,143 +32,256 @@ export type Notifier = {
|
|
|
26
32
|
notify(message: string, level: NotificationLevel): void
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
export type Confirmer = {
|
|
36
|
+
confirm(title: string, message: string): Promise<boolean>
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
export type StatusReporter = {
|
|
30
40
|
set(message: string): void
|
|
31
|
-
clear(): void
|
|
32
41
|
}
|
|
33
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
|
+
env: Env
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type EvaluationResult =
|
|
58
|
+
| {
|
|
59
|
+
ok: true
|
|
60
|
+
value: DiscoveryEvaluation
|
|
61
|
+
}
|
|
62
|
+
| { ok: false; error: unknown }
|
|
63
|
+
|
|
64
|
+
// A per-instance token suppresses stale writes when turns overlap
|
|
65
|
+
let latestToken: object = {}
|
|
66
|
+
|
|
34
67
|
// noinspection JSUnusedGlobalSymbols
|
|
35
68
|
export default function (pi: ExtensionAPI) {
|
|
36
69
|
pi.registerCommand(COMMAND_NAME, {
|
|
37
70
|
description: 'Dynamically discover Requesty models, run health checks, and update the local models.json.',
|
|
38
71
|
getArgumentCompletions,
|
|
39
72
|
handler: async (args, ctx) => {
|
|
40
|
-
await
|
|
73
|
+
await runDiscoveryWorkflow(ctx, args)
|
|
41
74
|
},
|
|
42
75
|
})
|
|
76
|
+
|
|
77
|
+
pi.on('turn_end', (_event, ctx) => {
|
|
78
|
+
void updateUsageStatus(ctx)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
pi.on('session_start', (_event, ctx) => {
|
|
82
|
+
void updateUsageStatus(ctx)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
pi.on('model_select', (_event, ctx) => {
|
|
86
|
+
void updateUsageStatus(ctx)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, args: string) {
|
|
91
|
+
// Interactive TUI command; no print/json/rpc path.
|
|
92
|
+
if (ctx.mode !== 'tui') {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const notifier = createUiNotifier(ctx)
|
|
97
|
+
const confirmer = createUiConfirmer(ctx)
|
|
98
|
+
|
|
99
|
+
// Phase A: progress UI only. Loader must close before confirm (Phase B).
|
|
100
|
+
// Errors are wrapped because ctx.ui.custom resolves via done() and does not reject.
|
|
101
|
+
const evaluationResult: EvaluationResult = await runWithStatusUi(ctx, 'Discovering models...', async status => {
|
|
102
|
+
try {
|
|
103
|
+
const evaluation = await evaluateDiscovery(args, status)
|
|
104
|
+
return { ok: true, value: evaluation }
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { ok: false, error }
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (!evaluationResult.ok) {
|
|
111
|
+
notifier.notify(formatDiscoveryFailure(evaluationResult.error), 'error')
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Phases B + C: decide, optional write, final notify — outside the loader.
|
|
116
|
+
await finalizeDiscovery(evaluationResult.value, confirmer, notifier)
|
|
43
117
|
}
|
|
44
118
|
|
|
45
119
|
async function runWithStatusUi<T>(
|
|
46
120
|
ctx: ExtensionCommandContext,
|
|
47
121
|
initialMessage: string,
|
|
48
|
-
fn: (status: StatusReporter
|
|
122
|
+
fn: (status: StatusReporter) => Promise<T>,
|
|
49
123
|
): Promise<T> {
|
|
50
|
-
if (ctx.mode !== 'tui') {
|
|
51
|
-
return fn(createNoopStatusReporter(), createNoopNotifier())
|
|
52
|
-
}
|
|
53
|
-
|
|
54
124
|
return ctx.ui.custom<T>((_tui, theme, _kb, done) => {
|
|
55
125
|
const loader = new RequestyStatusLoader(_tui, theme, initialMessage)
|
|
56
|
-
const notifier = createUiNotifier(ctx)
|
|
57
126
|
const status = createLoaderStatusReporter(loader)
|
|
58
127
|
void Promise.resolve()
|
|
59
|
-
.then(() => fn(status
|
|
128
|
+
.then(() => fn(status))
|
|
60
129
|
.then(done)
|
|
61
130
|
.catch(done)
|
|
62
131
|
return loader
|
|
63
132
|
})
|
|
64
133
|
}
|
|
65
134
|
|
|
66
|
-
|
|
135
|
+
function formatDiscoveryFailure(error: unknown): string {
|
|
136
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
137
|
+
return `Discovery failed: ${detail}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function evaluateDiscovery(args: string, status: StatusReporter): Promise<DiscoveryEvaluation> {
|
|
67
141
|
status.set('Discovering Requesty models...')
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
142
|
+
const dryRun = args.split(' ').includes(DRY_RUN_ARG)
|
|
143
|
+
|
|
144
|
+
const env = getEnv()
|
|
145
|
+
const { data, provider, existingModelIds } = getRequestyConfig(env)
|
|
146
|
+
const models = await discoverModels(provider)
|
|
147
|
+
const modelsMap = new Map(models.map(m => [m.id, m]))
|
|
148
|
+
|
|
149
|
+
let diff: ModelsDiff
|
|
150
|
+
let failedCount = 0
|
|
151
|
+
let passing: ProviderModelConfig[]
|
|
152
|
+
let logNote = ''
|
|
153
|
+
let healthCheckSummary = ''
|
|
154
|
+
|
|
155
|
+
if (env.health_check_mode !== 'off') {
|
|
156
|
+
status.set(`Checking models 0/${models.length}...`)
|
|
157
|
+
const healthResults = await checkModels(provider, models, env.health_check_mode === 'full', {
|
|
158
|
+
onProgress: ({ completed, total }) => {
|
|
159
|
+
status.set(`Checking models ${completed}/${total}...`)
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
|
|
163
|
+
failedCount = sortedResults.filter(r => !r.ok).length
|
|
164
|
+
passing = sortedResults.flatMap(r => {
|
|
165
|
+
const model = modelsMap.get(r.modelId)
|
|
166
|
+
return r.ok && model ? [model] : []
|
|
167
|
+
})
|
|
168
|
+
diff = diffModels(existingModelIds, passing)
|
|
169
|
+
healthCheckSummary = formatHealthSummary(sortedResults)
|
|
170
|
+
writeHealthCheckLog(provider, sortedResults, diff, env)
|
|
171
|
+
logNote = `Full health check log: ${env.health_check_log_path}\n`
|
|
172
|
+
} else {
|
|
173
|
+
passing = models
|
|
174
|
+
diff = diffModels(existingModelIds, passing)
|
|
72
175
|
}
|
|
73
176
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (env.health_check_mode !== 'off') {
|
|
87
|
-
status.set(`Checking models 0/${models.length}...`)
|
|
88
|
-
const healthResults = await checkModels(provider, models, env.health_check_mode === 'full', {
|
|
89
|
-
onProgress: ({ completed, total }) => {
|
|
90
|
-
status.set(`Checking models ${completed}/${total}...`)
|
|
91
|
-
},
|
|
92
|
-
})
|
|
93
|
-
const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
|
|
94
|
-
failed = sortedResults.filter(r => !r.ok)
|
|
95
|
-
passing = sortedResults.flatMap(r => {
|
|
96
|
-
const model = modelsMap.get(r.modelId)
|
|
97
|
-
return r.ok && model ? [model] : []
|
|
98
|
-
})
|
|
99
|
-
diff = diffModels(existingModelIds, passing)
|
|
100
|
-
healthCheckSummary = formatHealthSummary(sortedResults)
|
|
101
|
-
writeHealthCheckLog(provider, sortedResults, diff, env)
|
|
102
|
-
logNote = `Full health check log: ${env.health_check_log_path}\n`
|
|
103
|
-
} else {
|
|
104
|
-
passing = models
|
|
105
|
-
diff = diffModels(existingModelIds, passing)
|
|
106
|
-
}
|
|
177
|
+
return {
|
|
178
|
+
dryRun,
|
|
179
|
+
modelCount: models.length,
|
|
180
|
+
failedCount,
|
|
181
|
+
passing,
|
|
182
|
+
diff,
|
|
183
|
+
healthCheckSummary,
|
|
184
|
+
logNote,
|
|
185
|
+
data,
|
|
186
|
+
env,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
107
189
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
190
|
+
async function finalizeDiscovery(
|
|
191
|
+
evaluation: DiscoveryEvaluation,
|
|
192
|
+
confirmer: Confirmer,
|
|
193
|
+
notifier: Notifier,
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
const level = notificationLevel(evaluation)
|
|
196
|
+
const summary = buildDiscoverySummary(evaluation)
|
|
112
197
|
|
|
113
|
-
|
|
114
|
-
|
|
198
|
+
// Always surface the discovery result first (toast styling), then decide.
|
|
199
|
+
if (evaluation.dryRun) {
|
|
200
|
+
notifier.notify(
|
|
201
|
+
`${summary}
|
|
202
|
+
Dry run: left models.json unchanged.`,
|
|
203
|
+
level,
|
|
204
|
+
)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
115
207
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
208
|
+
if (evaluation.passing.length === 0) {
|
|
209
|
+
notifier.notify(
|
|
210
|
+
`${summary}
|
|
211
|
+
Left models.json unchanged.`,
|
|
212
|
+
level,
|
|
213
|
+
)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
notifier.notify(summary, level)
|
|
218
|
+
const { title, message } = buildConfirmPrompt(evaluation)
|
|
219
|
+
const shouldUpdate = await confirmer.confirm(title, message)
|
|
220
|
+
if (shouldUpdate) {
|
|
221
|
+
updateModelsJson(evaluation.data, evaluation.passing, evaluation.env)
|
|
222
|
+
notifier.notify('Updated models.json. Run /reload to use the changes.', 'info')
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
notifier.notify('Left models.json unchanged.', 'info')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildDiscoverySummary(evaluation: DiscoveryEvaluation): string {
|
|
230
|
+
return [
|
|
231
|
+
`Discovered ${evaluation.modelCount} Requesty model(s).`,
|
|
232
|
+
evaluation.healthCheckSummary.trimEnd(),
|
|
233
|
+
formatModelsDiffSummary(evaluation.diff),
|
|
234
|
+
evaluation.logNote.trimEnd(),
|
|
235
|
+
]
|
|
236
|
+
.filter(part => part.length > 0)
|
|
237
|
+
.join('\n')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildConfirmPrompt(evaluation: DiscoveryEvaluation): { title: string; message: string } {
|
|
241
|
+
const hasIdChanges = evaluation.diff.added.length > 0 || evaluation.diff.removed.length > 0
|
|
242
|
+
if (hasIdChanges) {
|
|
243
|
+
return {
|
|
244
|
+
title: `Write ${evaluation.passing.length} model(s)?`,
|
|
245
|
+
message: 'Update models.json with the discovery result above.',
|
|
122
246
|
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
title: 'Refresh models.json?',
|
|
250
|
+
message: 'No model ID changes. Rewrite the file to refresh metadata anyway?',
|
|
127
251
|
}
|
|
128
252
|
}
|
|
129
253
|
|
|
254
|
+
function notificationLevel(evaluation: DiscoveryEvaluation): NotificationLevel {
|
|
255
|
+
if (evaluation.failedCount === 0) return 'info'
|
|
256
|
+
if (evaluation.failedCount < evaluation.modelCount) return 'warning'
|
|
257
|
+
return 'error'
|
|
258
|
+
}
|
|
259
|
+
|
|
130
260
|
function getArgumentCompletions(prefix: string): AutocompleteItem[] {
|
|
131
261
|
const options = [
|
|
132
262
|
{
|
|
133
263
|
value: DRY_RUN_ARG,
|
|
134
264
|
label: DRY_RUN_ARG,
|
|
135
|
-
description: 'Preview without
|
|
265
|
+
description: 'Preview discovery without offering to write models.json',
|
|
136
266
|
},
|
|
137
267
|
]
|
|
138
268
|
if (!prefix) return options
|
|
139
269
|
return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
140
270
|
}
|
|
141
271
|
|
|
142
|
-
function
|
|
143
|
-
return {
|
|
144
|
-
set() {
|
|
145
|
-
// Status output is only rendered in TUI mode.
|
|
146
|
-
},
|
|
147
|
-
clear() {
|
|
148
|
-
// Status output is only rendered in TUI mode.
|
|
149
|
-
},
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function createNoopNotifier(): Notifier {
|
|
272
|
+
function createUiNotifier(ctx: ExtensionCommandContext): Notifier {
|
|
154
273
|
return {
|
|
155
|
-
notify() {
|
|
156
|
-
|
|
274
|
+
notify(message: string, level?: NotificationLevel) {
|
|
275
|
+
const prefixedMessage = `${COMMAND_NAME}: ${message}`
|
|
276
|
+
ctx.ui.notify(prefixedMessage, level)
|
|
157
277
|
},
|
|
158
278
|
}
|
|
159
279
|
}
|
|
160
280
|
|
|
161
|
-
function
|
|
281
|
+
function createUiConfirmer(ctx: ExtensionCommandContext): Confirmer {
|
|
162
282
|
return {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
ctx.ui.notify(prefixedMessage, level)
|
|
283
|
+
confirm(title, message) {
|
|
284
|
+
return ctx.ui.confirm(title, message)
|
|
166
285
|
},
|
|
167
286
|
}
|
|
168
287
|
}
|
|
@@ -172,8 +291,50 @@ function createLoaderStatusReporter(loader: RequestyStatusLoader): StatusReporte
|
|
|
172
291
|
set(message: string) {
|
|
173
292
|
loader.setMessage(message)
|
|
174
293
|
},
|
|
175
|
-
clear() {
|
|
176
|
-
// Nothing to clear: the loader closes when ctx.ui.custom() resolves.
|
|
177
|
-
},
|
|
178
294
|
}
|
|
179
295
|
}
|
|
296
|
+
|
|
297
|
+
export async function updateUsageStatus(ctx: ExtensionContext): Promise<void> {
|
|
298
|
+
if (!ctx.hasUI) return // no footer to write to (print/json mode): skip the wasted fetch
|
|
299
|
+
const token: object = {}
|
|
300
|
+
latestToken = token
|
|
301
|
+
const providerId = getRequestyProviderId()
|
|
302
|
+
if (providerId === undefined) return // not configured for Requesty: nothing to do
|
|
303
|
+
const shouldClear = ctx.model?.provider !== providerId
|
|
304
|
+
try {
|
|
305
|
+
if (shouldClear) {
|
|
306
|
+
ctx.ui.setStatus(USAGE_STATUS_KEY, undefined)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
const info = await fetchUsageStatus()
|
|
310
|
+
if (latestToken !== token) return
|
|
311
|
+
ctx.ui.setStatus(USAGE_STATUS_KEY, formatUsageStatus(info))
|
|
312
|
+
} catch {
|
|
313
|
+
// best-effort footer update: swallow fetch errors and stale-ctx throws (e.g. after /reload)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Returns the configured Requesty provider id, or undefined when Requesty is not configured. */
|
|
318
|
+
function getRequestyProviderId(): string | undefined {
|
|
319
|
+
try {
|
|
320
|
+
return getEnv().provider_id
|
|
321
|
+
} catch {
|
|
322
|
+
return undefined
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function formatUsageStatus(info: ApiKeyInfo): string {
|
|
327
|
+
const spend = `$${info.monthlySpend.toFixed(2)}`
|
|
328
|
+
if (info.monthlyLimit <= 0) {
|
|
329
|
+
return `Requesty Usage (${info.name}): ${spend} (unlimited)`
|
|
330
|
+
}
|
|
331
|
+
const limit = `$${info.monthlyLimit.toFixed(2)}`
|
|
332
|
+
const percent = Math.floor((info.monthlySpend / info.monthlyLimit) * 100)
|
|
333
|
+
return `Requesty Usage (${info.name}): ${spend}/${limit} (${percent}%)`
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function fetchUsageStatus(): Promise<ApiKeyInfo> {
|
|
337
|
+
const env = getEnv()
|
|
338
|
+
const { provider } = getRequestyConfig(env)
|
|
339
|
+
return await fetchApiUsage(provider)
|
|
340
|
+
}
|
package/src/requesty-api.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from 'zod'
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_CONTEXT_WINDOW = 128000
|
|
5
5
|
const DEFAULT_MAX_TOKENS = 4096
|
|
6
|
+
const REQUESTY_MANAGE_URL = 'https://api-v2.requesty.ai/v1/manage'
|
|
6
7
|
|
|
7
8
|
const RequestyModelSchema = z
|
|
8
9
|
.object({
|
|
@@ -66,3 +67,31 @@ function pricePerMillionTokens(value: number | null | undefined): number {
|
|
|
66
67
|
const raw = (value ?? 0) * 1_000_000
|
|
67
68
|
return Number(raw.toFixed(3))
|
|
68
69
|
}
|
|
70
|
+
|
|
71
|
+
const ApiKeyInfoSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
name: z.string().min(1),
|
|
74
|
+
monthly_spend: z.coerce.number(),
|
|
75
|
+
monthly_limit: z.coerce.number(),
|
|
76
|
+
})
|
|
77
|
+
.transform(value => ({
|
|
78
|
+
name: value.name,
|
|
79
|
+
monthlySpend: value.monthly_spend,
|
|
80
|
+
monthlyLimit: value.monthly_limit,
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
export type ApiKeyInfo = z.infer<typeof ApiKeyInfoSchema>
|
|
84
|
+
|
|
85
|
+
export async function fetchApiUsage(provider: Provider): Promise<ApiKeyInfo> {
|
|
86
|
+
const url = `${REQUESTY_MANAGE_URL}/apikey/self`
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
headers: { Authorization: `Bearer ${provider.apiKey}` },
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rawData = await response.json()
|
|
96
|
+
return ApiKeyInfoSchema.parse(rawData)
|
|
97
|
+
}
|