pi-requesty-discover 2.0.0 β 2.1.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/CONTRIBUTING.adoc +1 -0
- package/README.adoc +22 -0
- package/README.md +21 -0
- package/docs/DECISIONS.adoc +52 -0
- package/package.json +1 -1
- package/src/env.ts +3 -1
- package/src/index.ts +123 -31
- package/src/models-json.ts +1 -1
- package/src/requesty-api.ts +37 -1
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
|
@@ -18,6 +18,7 @@ This is a fork of https://github.com/requestyai/pi-requesty[pi-requesty] with ad
|
|
|
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
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
|
|
21
22
|
|
|
22
23
|
== When and why use it
|
|
23
24
|
|
|
@@ -27,6 +28,7 @@ Use this extension if
|
|
|
27
28
|
- you are limited to certain models by a company policy (for instance, for data residency/protection reasons)
|
|
28
29
|
- you don't want to handcraft your own curated list of models constantly
|
|
29
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
|
|
30
32
|
|
|
31
33
|
== Installation
|
|
32
34
|
|
|
@@ -142,3 +144,23 @@ After a successful write, run `/reload`.
|
|
|
142
144
|
----
|
|
143
145
|
|
|
144
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
|
@@ -10,6 +10,7 @@ This is a fork of [pi-requesty](https://github.com/requestyai/pi-requesty) with
|
|
|
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
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
|
|
13
14
|
|
|
14
15
|
## When and why use it
|
|
15
16
|
|
|
@@ -19,6 +20,7 @@ Use this extension if
|
|
|
19
20
|
* you are limited to certain models by a company policy (for instance, for data residency/protection reasons)
|
|
20
21
|
* you donβt want to handcraft your own curated list of models constantly
|
|
21
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
|
|
22
24
|
|
|
23
25
|
## Installation
|
|
24
26
|
|
|
@@ -108,3 +110,22 @@ After a successful write, run `/reload`.
|
|
|
108
110
|
```
|
|
109
111
|
|
|
110
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,52 @@
|
|
|
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
|
+
== Hand-rolled `Try<T>` instead of an effects library
|
|
18
|
+
|
|
19
|
+
Context: env loading and discovery evaluation both need a caught-error result value
|
|
20
|
+
(`{ ok: true, value } | { ok: false, error }`). Libraries like Effect ship an
|
|
21
|
+
industrial `Either`/`Effect.try` for exactly this.
|
|
22
|
+
|
|
23
|
+
Why not: Effect is a runtime, a generator DSL, and a worldview β not a utility.
|
|
24
|
+
Two call sites in a Pi extension don't justify the dependency weight or the
|
|
25
|
+
learning curve for contributors. The hand-rolled version is ~10 lines
|
|
26
|
+
(`Try<T>`, `runCatching`, `runCatchingAsync`, named after Kotlin's `Result`),
|
|
27
|
+
lives in `src/index.ts`, and documents intent just as well.
|
|
28
|
+
|
|
29
|
+
Decision: keep the hand-rolled `Try<T>`. No `monads.ts` module either β rule of
|
|
30
|
+
three. Revisit if the extension ever needs typed errors, retries, or resource
|
|
31
|
+
scoping; that's the day Effect earns its place.
|
|
32
|
+
|
|
33
|
+
== Not a provider β a models.json-writing extension
|
|
34
|
+
|
|
35
|
+
Context: Requesty already works as a Pi provider via `models.json`. The temptation
|
|
36
|
+
is to make this extension *be* the provider: read the model list from the Requesty
|
|
37
|
+
API, hold it in memory, surface usage stats, let Pi manage the API key.
|
|
38
|
+
|
|
39
|
+
Why not:
|
|
40
|
+
|
|
41
|
+
- Health-checking is the core value and has no home in a provider. A provider serves
|
|
42
|
+
models; it doesn't vet them. The health check plus the diff/confirm/write flow is a
|
|
43
|
+
discovery-and-selection step, not serving.
|
|
44
|
+
- A provider keeps its model list in-memory and refreshes occasionally. Here the
|
|
45
|
+
list is a persistent on-disk snapshot (`models.json`) β constant until you re-run
|
|
46
|
+
`/requesty-discover`. That matches the intent: an explicit, reviewed, user-confirmed write,
|
|
47
|
+
after which Pi's normal provider machinery serves the models.
|
|
48
|
+
|
|
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.
|
|
51
|
+
|
|
52
|
+
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,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
ProviderModelConfig,
|
|
6
|
+
} from '@earendil-works/pi-coding-agent'
|
|
2
7
|
import { type Env, getEnv } from './env'
|
|
3
8
|
import {
|
|
4
9
|
diffModels,
|
|
@@ -7,12 +12,13 @@ import {
|
|
|
7
12
|
type ModelsDiff,
|
|
8
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
|
|
@@ -45,28 +51,42 @@ type DiscoveryEvaluation = {
|
|
|
45
51
|
healthCheckSummary: string
|
|
46
52
|
logNote: string
|
|
47
53
|
data: ModelsJsonData
|
|
48
|
-
env: Env
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
type
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
| { ok: false; error: unknown }
|
|
56
|
+
export type Try<T> = { ok: true; value: T } | { ok: false; error: unknown }
|
|
57
|
+
|
|
58
|
+
// token suppresses stale writes when turns overlap
|
|
59
|
+
let latestToken: object = {}
|
|
57
60
|
|
|
58
61
|
// noinspection JSUnusedGlobalSymbols
|
|
59
62
|
export default function (pi: ExtensionAPI) {
|
|
63
|
+
const env = runCatching(() => getEnv())
|
|
64
|
+
|
|
60
65
|
pi.registerCommand(COMMAND_NAME, {
|
|
61
66
|
description: 'Dynamically discover Requesty models, run health checks, and update the local models.json.',
|
|
62
67
|
getArgumentCompletions,
|
|
63
68
|
handler: async (args, ctx) => {
|
|
64
|
-
await runDiscoveryWorkflow(ctx, args)
|
|
69
|
+
await runDiscoveryWorkflow(ctx, env, args)
|
|
65
70
|
},
|
|
66
71
|
})
|
|
72
|
+
|
|
73
|
+
pi.on('turn_end', (_event, ctx) => {
|
|
74
|
+
void updateUsageStatus(ctx, env)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
pi.on('session_start', (_event, ctx) => {
|
|
78
|
+
complainOnBrokenEnv(ctx, env)
|
|
79
|
+
void updateUsageStatus(ctx, env)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
pi.on('model_select', (_event, ctx) => {
|
|
83
|
+
void updateUsageStatus(ctx, env)
|
|
84
|
+
})
|
|
67
85
|
}
|
|
68
86
|
|
|
69
|
-
export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, args: string) {
|
|
87
|
+
export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, env: Try<Env>, args: string) {
|
|
88
|
+
complainOnBrokenEnv(ctx, env)
|
|
89
|
+
if (!env.ok) return
|
|
70
90
|
// Interactive TUI command; no print/json/rpc path.
|
|
71
91
|
if (ctx.mode !== 'tui') {
|
|
72
92
|
return
|
|
@@ -77,14 +97,11 @@ export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, args: s
|
|
|
77
97
|
|
|
78
98
|
// Phase A: progress UI only. Loader must close before confirm (Phase B).
|
|
79
99
|
// Errors are wrapped because ctx.ui.custom resolves via done() and does not reject.
|
|
80
|
-
const evaluationResult:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return { ok: false, error }
|
|
86
|
-
}
|
|
87
|
-
})
|
|
100
|
+
const evaluationResult: Try<DiscoveryEvaluation> = await runWithStatusUi(
|
|
101
|
+
ctx,
|
|
102
|
+
'Discovering models...',
|
|
103
|
+
async status => await runCatchingAsync(() => evaluateDiscovery(args, env.value, status)),
|
|
104
|
+
)
|
|
88
105
|
|
|
89
106
|
if (!evaluationResult.ok) {
|
|
90
107
|
notifier.notify(formatDiscoveryFailure(evaluationResult.error), 'error')
|
|
@@ -92,7 +109,12 @@ export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, args: s
|
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
// Phases B + C: decide, optional write, final notify β outside the loader.
|
|
95
|
-
await finalizeDiscovery(evaluationResult.value, confirmer, notifier)
|
|
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
118
|
}
|
|
97
119
|
|
|
98
120
|
async function runWithStatusUi<T>(
|
|
@@ -111,16 +133,10 @@ async function runWithStatusUi<T>(
|
|
|
111
133
|
})
|
|
112
134
|
}
|
|
113
135
|
|
|
114
|
-
function
|
|
115
|
-
const detail = error instanceof Error ? error.message : String(error)
|
|
116
|
-
return `Discovery failed: ${detail}`
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function evaluateDiscovery(args: string, status: StatusReporter): Promise<DiscoveryEvaluation> {
|
|
136
|
+
async function evaluateDiscovery(args: string, env: Env, status: StatusReporter): Promise<DiscoveryEvaluation> {
|
|
120
137
|
status.set('Discovering Requesty models...')
|
|
121
138
|
const dryRun = args.split(' ').includes(DRY_RUN_ARG)
|
|
122
139
|
|
|
123
|
-
const env = getEnv()
|
|
124
140
|
const { data, provider, existingModelIds } = getRequestyConfig(env)
|
|
125
141
|
const models = await discoverModels(provider)
|
|
126
142
|
const modelsMap = new Map(models.map(m => [m.id, m]))
|
|
@@ -162,7 +178,6 @@ async function evaluateDiscovery(args: string, status: StatusReporter): Promise<
|
|
|
162
178
|
healthCheckSummary,
|
|
163
179
|
logNote,
|
|
164
180
|
data,
|
|
165
|
-
env,
|
|
166
181
|
}
|
|
167
182
|
}
|
|
168
183
|
|
|
@@ -170,6 +185,7 @@ async function finalizeDiscovery(
|
|
|
170
185
|
evaluation: DiscoveryEvaluation,
|
|
171
186
|
confirmer: Confirmer,
|
|
172
187
|
notifier: Notifier,
|
|
188
|
+
env: Env,
|
|
173
189
|
): Promise<void> {
|
|
174
190
|
const level = notificationLevel(evaluation)
|
|
175
191
|
const summary = buildDiscoverySummary(evaluation)
|
|
@@ -197,7 +213,7 @@ Left models.json unchanged.`,
|
|
|
197
213
|
const { title, message } = buildConfirmPrompt(evaluation)
|
|
198
214
|
const shouldUpdate = await confirmer.confirm(title, message)
|
|
199
215
|
if (shouldUpdate) {
|
|
200
|
-
updateModelsJson(evaluation.data, evaluation.passing,
|
|
216
|
+
updateModelsJson(evaluation.data, evaluation.passing, env)
|
|
201
217
|
notifier.notify('Updated models.json. Run /reload to use the changes.', 'info')
|
|
202
218
|
return
|
|
203
219
|
}
|
|
@@ -248,7 +264,7 @@ function getArgumentCompletions(prefix: string): AutocompleteItem[] {
|
|
|
248
264
|
return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
249
265
|
}
|
|
250
266
|
|
|
251
|
-
function createUiNotifier(ctx:
|
|
267
|
+
function createUiNotifier(ctx: ExtensionContext): Notifier {
|
|
252
268
|
return {
|
|
253
269
|
notify(message: string, level?: NotificationLevel) {
|
|
254
270
|
const prefixedMessage = `${COMMAND_NAME}: ${message}`
|
|
@@ -257,7 +273,7 @@ function createUiNotifier(ctx: ExtensionCommandContext): Notifier {
|
|
|
257
273
|
}
|
|
258
274
|
}
|
|
259
275
|
|
|
260
|
-
function createUiConfirmer(ctx:
|
|
276
|
+
function createUiConfirmer(ctx: ExtensionContext): Confirmer {
|
|
261
277
|
return {
|
|
262
278
|
confirm(title, message) {
|
|
263
279
|
return ctx.ui.confirm(title, message)
|
|
@@ -272,3 +288,79 @@ function createLoaderStatusReporter(loader: RequestyStatusLoader): StatusReporte
|
|
|
272
288
|
},
|
|
273
289
|
}
|
|
274
290
|
}
|
|
291
|
+
|
|
292
|
+
async function updateUsageStatus(ctx: ExtensionContext, env: Try<Env>): Promise<void> {
|
|
293
|
+
if (!env.ok) return
|
|
294
|
+
if (!ctx.hasUI) return // no footer to write to (print/json mode): skip the wasted fetch
|
|
295
|
+
const token: object = {}
|
|
296
|
+
latestToken = token
|
|
297
|
+
const shouldClear = ctx.model?.provider !== env.value.provider_id
|
|
298
|
+
try {
|
|
299
|
+
if (shouldClear) {
|
|
300
|
+
ctx.ui.setStatus(USAGE_STATUS_KEY, undefined)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
const info = await fetchUsageStatus(env.value)
|
|
304
|
+
if (latestToken !== token) return
|
|
305
|
+
ctx.ui.setStatus(USAGE_STATUS_KEY, formatUsageStatus(info))
|
|
306
|
+
} catch {
|
|
307
|
+
// best-effort footer update: swallow fetch errors and stale-ctx throws (e.g. after /reload)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function formatUsageStatus(info: ApiKeyInfo): string {
|
|
312
|
+
const spend = `$${info.monthlySpend.toFixed(2)}`
|
|
313
|
+
if (info.monthlyLimit <= 0) {
|
|
314
|
+
return `Requesty Usage (${info.name}): ${spend} (unlimited)`
|
|
315
|
+
}
|
|
316
|
+
const limit = `$${info.monthlyLimit.toFixed(2)}`
|
|
317
|
+
const percent = Math.floor((info.monthlySpend / info.monthlyLimit) * 100)
|
|
318
|
+
return `Requesty Usage (${info.name}): ${spend}/${limit} (${percent}%)`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let lastFetched: { value: ApiKeyInfo; time: Date } | undefined
|
|
322
|
+
|
|
323
|
+
async function fetchUsageStatus(env: Env): Promise<ApiKeyInfo> {
|
|
324
|
+
const now = new Date()
|
|
325
|
+
if (lastFetched?.time && now.getTime() - lastFetched.time.getTime() < 2000) {
|
|
326
|
+
return lastFetched.value
|
|
327
|
+
}
|
|
328
|
+
const { provider } = getRequestyConfig(env)
|
|
329
|
+
const value = await fetchApiUsage(provider)
|
|
330
|
+
lastFetched = { value, time: new Date() }
|
|
331
|
+
return value
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function formatError(error: unknown) {
|
|
335
|
+
return error instanceof Error ? error.message : String(error)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* For testing purposes, reset the cache after each run
|
|
340
|
+
*/
|
|
341
|
+
export function resetUsageStatusCache(): void {
|
|
342
|
+
lastFetched = undefined
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function runCatching<T>(fn: () => T): Try<T> {
|
|
346
|
+
try {
|
|
347
|
+
return { ok: true, value: fn() }
|
|
348
|
+
} catch (error) {
|
|
349
|
+
return { ok: false, error }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function runCatchingAsync<T>(fn: () => Promise<T>): Promise<Try<T>> {
|
|
354
|
+
try {
|
|
355
|
+
return { ok: true, value: await fn() }
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return { ok: false, error }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function complainOnBrokenEnv(ctx: ExtensionContext, env: Try<Env>) {
|
|
362
|
+
if (!env.ok) {
|
|
363
|
+
const notifier = createUiNotifier(ctx)
|
|
364
|
+
notifier.notify(`failed to load env: ${formatError(env.error)}`, 'error')
|
|
365
|
+
}
|
|
366
|
+
}
|
package/src/models-json.ts
CHANGED
|
@@ -25,7 +25,7 @@ const ModelsJsonSchema = z
|
|
|
25
25
|
type ModelsJson = z.infer<typeof ModelsJsonSchema>
|
|
26
26
|
type ModelsJsonProvider = z.infer<typeof ProviderSchema>
|
|
27
27
|
|
|
28
|
-
type RequestyProvider = ModelsJsonProvider & {
|
|
28
|
+
export type RequestyProvider = ModelsJsonProvider & {
|
|
29
29
|
name: string
|
|
30
30
|
baseUrl: string
|
|
31
31
|
apiKey: string
|
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({
|
|
@@ -30,9 +31,14 @@ type Provider = {
|
|
|
30
31
|
apiKey: string
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export async function discoverModels(
|
|
34
|
+
export async function discoverModels(
|
|
35
|
+
provider: Provider,
|
|
36
|
+
options: { timeoutMs?: number } = {},
|
|
37
|
+
): Promise<ProviderModelConfig[]> {
|
|
38
|
+
const timeoutMs = options.timeoutMs ?? 5_000
|
|
34
39
|
const response = await fetch(`${provider.baseUrl}/models`, {
|
|
35
40
|
headers: { Authorization: `Bearer ${provider.apiKey}` },
|
|
41
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
36
42
|
})
|
|
37
43
|
|
|
38
44
|
if (!response.ok) {
|
|
@@ -66,3 +72,33 @@ function pricePerMillionTokens(value: number | null | undefined): number {
|
|
|
66
72
|
const raw = (value ?? 0) * 1_000_000
|
|
67
73
|
return Number(raw.toFixed(3))
|
|
68
74
|
}
|
|
75
|
+
|
|
76
|
+
const ApiKeyInfoSchema = z
|
|
77
|
+
.object({
|
|
78
|
+
name: z.string().min(1),
|
|
79
|
+
monthly_spend: z.coerce.number(),
|
|
80
|
+
monthly_limit: z.coerce.number(),
|
|
81
|
+
})
|
|
82
|
+
.transform(value => ({
|
|
83
|
+
name: value.name,
|
|
84
|
+
monthlySpend: value.monthly_spend,
|
|
85
|
+
monthlyLimit: value.monthly_limit,
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
export type ApiKeyInfo = z.infer<typeof ApiKeyInfoSchema>
|
|
89
|
+
|
|
90
|
+
export async function fetchApiUsage(provider: Provider, options: { timeoutMs?: number } = {}): Promise<ApiKeyInfo> {
|
|
91
|
+
const url = `${REQUESTY_MANAGE_URL}/apikey/self`
|
|
92
|
+
const timeoutMs = options.timeoutMs ?? 2_000
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
headers: { Authorization: `Bearer ${provider.apiKey}` },
|
|
95
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const rawData = await response.json()
|
|
103
|
+
return ApiKeyInfoSchema.parse(rawData)
|
|
104
|
+
}
|