pi-requesty-discover 3.0.2 → 3.1.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-requesty-discover",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Discover available Requesty models for the Pi Coding Agent",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
]
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-tui": "^0.85.0",
|
|
54
53
|
"@earendil-works/pi-coding-agent": "^0.85.0",
|
|
54
|
+
"@earendil-works/pi-tui": "^0.85.0",
|
|
55
55
|
"@eslint/js": "^10.0.1",
|
|
56
56
|
"@types/node": "^26.1.0",
|
|
57
57
|
"@vitest/coverage-v8": "^4.1.9",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"vitest": "^4.1.9"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
+
"openai": "^7.10.0",
|
|
70
71
|
"json5": "^2.2.3",
|
|
71
72
|
"zod": "^4.4.3"
|
|
72
73
|
}
|
package/src/health-check.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ProviderModelConfig } from '@earendil-works/pi-coding-agent'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import OpenAI from 'openai'
|
|
5
|
+
import type { Stream } from 'openai/streaming'
|
|
4
6
|
import { type Env, getEnv } from './env'
|
|
5
7
|
import { formatModelsDiffSummary, type ModelsDiff } from './models-json'
|
|
8
|
+
import { formatErrorMessage } from './utils'
|
|
6
9
|
|
|
7
10
|
const HEALTH_CHECK_CONCURRENCY = 10
|
|
8
11
|
const HEALTH_CHECK_TIMEOUT_MS = 15_000
|
|
@@ -16,8 +19,6 @@ const defaultHealthCheckOptions = {
|
|
|
16
19
|
retryDelayMs: HEALTH_CHECK_RETRY_DELAY_MS,
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
const DATA_PREFIX = 'data:'
|
|
20
|
-
|
|
21
22
|
export type Provider = {
|
|
22
23
|
baseUrl: string
|
|
23
24
|
apiKey: string
|
|
@@ -170,53 +171,35 @@ export function writeHealthCheckLog(
|
|
|
170
171
|
|
|
171
172
|
export async function postChatCompletion(
|
|
172
173
|
provider: Provider,
|
|
173
|
-
body:
|
|
174
|
+
body: Omit<OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, 'stream'>,
|
|
174
175
|
options: HealthCheckOptions = {},
|
|
175
176
|
): Promise<ModelCheckResult> {
|
|
176
177
|
const healthCheckOptions = resolveHealthCheckOptions(options)
|
|
177
178
|
const start = Date.now()
|
|
179
|
+
const client = createClient(provider, healthCheckOptions)
|
|
178
180
|
|
|
179
181
|
for (let attempt = 0; attempt <= healthCheckOptions.retries; attempt++) {
|
|
180
182
|
try {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Authorization: `Bearer ${provider.apiKey}`,
|
|
185
|
-
'Content-Type': 'application/json',
|
|
186
|
-
Accept: 'text/event-stream',
|
|
187
|
-
},
|
|
188
|
-
body: JSON.stringify({ ...body, stream: true }),
|
|
189
|
-
signal: AbortSignal.timeout(healthCheckOptions.timeoutMs),
|
|
183
|
+
const stream = await client.chat.completions.create({
|
|
184
|
+
...body,
|
|
185
|
+
stream: true,
|
|
190
186
|
})
|
|
191
|
-
|
|
192
|
-
const latencyMs = Date.now() - start
|
|
193
|
-
|
|
194
|
-
if (!response.ok) {
|
|
195
|
-
const text = await response.text().catch(() => '')
|
|
196
|
-
// rate limits are transient - a warning, not an error
|
|
197
|
-
const status: HealthCheckStatus = response.status === 429 ? 'warning' : 'error'
|
|
198
|
-
return {
|
|
199
|
-
status,
|
|
200
|
-
latencyMs,
|
|
201
|
-
error: `HTTP ${response.status} ${response.statusText}${text ? `: ${text}` : ''}`,
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return await verifyFirstStreamChunk(response.body!, start)
|
|
187
|
+
return await verifyFirstStreamChunk(stream, start)
|
|
206
188
|
} catch (err) {
|
|
207
|
-
if (
|
|
189
|
+
if (isTimeout(err) && attempt < healthCheckOptions.retries) {
|
|
208
190
|
if (healthCheckOptions.retryDelayMs > 0) {
|
|
209
191
|
await new Promise(resolve => setTimeout(resolve, healthCheckOptions.retryDelayMs))
|
|
210
192
|
}
|
|
211
193
|
continue
|
|
212
194
|
}
|
|
213
195
|
const attempts = attempt + 1
|
|
196
|
+
const status: HealthCheckStatus = isTransient(err) ? 'warning' : 'error'
|
|
214
197
|
return {
|
|
215
|
-
status
|
|
198
|
+
status,
|
|
216
199
|
latencyMs: Date.now() - start,
|
|
217
|
-
error:
|
|
200
|
+
error: isTimeout(err)
|
|
218
201
|
? `Timed out after ${attempts} attempt(s); per-attempt timeout is ${healthCheckOptions.timeoutMs / 1000}s`
|
|
219
|
-
:
|
|
202
|
+
: formatRequestError(err),
|
|
220
203
|
}
|
|
221
204
|
}
|
|
222
205
|
}
|
|
@@ -224,41 +207,43 @@ export async function postChatCompletion(
|
|
|
224
207
|
return { status: 'error', latencyMs: Date.now() - start, error: 'Unknown error' }
|
|
225
208
|
}
|
|
226
209
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
210
|
+
function isTimeout(err: unknown) {
|
|
211
|
+
return err instanceof OpenAI.APIConnectionTimeoutError
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isTransient(err: unknown) {
|
|
215
|
+
return err instanceof OpenAI.RateLimitError
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function formatRequestError(err: unknown) {
|
|
219
|
+
if (err instanceof OpenAI.APIError && err.status !== undefined) {
|
|
220
|
+
// for JSON bodies err.error holds the (unwrapped) parsed payload; for text bodies err.message
|
|
221
|
+
// already includes the status and the raw response text
|
|
222
|
+
const body = err.error ? `: ${JSON.stringify(err.error)}` : `: ${err.message}`
|
|
223
|
+
return `HTTP ${err.status}${body}`
|
|
224
|
+
}
|
|
225
|
+
return formatErrorMessage(err)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createClient(provider: Provider, options: ResolvedHealthCheckOptions): OpenAI {
|
|
229
|
+
return new OpenAI({
|
|
230
|
+
apiKey: provider.apiKey,
|
|
231
|
+
baseURL: provider.baseUrl,
|
|
232
|
+
timeout: options.timeoutMs,
|
|
233
|
+
maxRetries: 0,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function verifyFirstStreamChunk(
|
|
238
|
+
stream: Stream<OpenAI.Chat.Completions.ChatCompletionChunk>,
|
|
239
|
+
start: number,
|
|
240
|
+
): Promise<ModelCheckResult> {
|
|
241
|
+
for await (const chunk of stream) {
|
|
242
|
+
if (chunk.choices instanceof Array && chunk.choices.length > 0) {
|
|
243
|
+
return { status: 'ok', latencyMs: Date.now() - start }
|
|
257
244
|
}
|
|
258
|
-
return { status: 'error', latencyMs: Date.now() - start, error: 'Stream ended without content' }
|
|
259
|
-
} finally {
|
|
260
|
-
await reader.cancel()
|
|
261
245
|
}
|
|
246
|
+
return { status: 'error', latencyMs: Date.now() - start, error: 'Stream ended without content' }
|
|
262
247
|
}
|
|
263
248
|
|
|
264
249
|
async function checkModel(
|
|
@@ -325,7 +310,3 @@ function resolveHealthCheckOptions(options: HealthCheckOptions): ResolvedHealthC
|
|
|
325
310
|
...options,
|
|
326
311
|
}
|
|
327
312
|
}
|
|
328
|
-
|
|
329
|
-
function isTimeoutError(error: unknown): boolean {
|
|
330
|
-
return error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')
|
|
331
|
-
}
|
package/src/models-json.ts
CHANGED
|
@@ -23,9 +23,15 @@ const ProviderSchema = z
|
|
|
23
23
|
|
|
24
24
|
const ModelsJsonSchema = z
|
|
25
25
|
.object({
|
|
26
|
+
$schema: z.string().optional(),
|
|
26
27
|
providers: z.record(z.string(), ProviderSchema),
|
|
27
28
|
})
|
|
28
29
|
.catchall(z.unknown())
|
|
30
|
+
.transform(({ $schema, providers, ...rest }) => ({
|
|
31
|
+
...($schema !== undefined ? { $schema } : {}),
|
|
32
|
+
...rest,
|
|
33
|
+
providers,
|
|
34
|
+
}))
|
|
29
35
|
|
|
30
36
|
export type ModelsJson = z.infer<typeof ModelsJsonSchema>
|
|
31
37
|
type ModelsJsonProvider = z.infer<typeof ProviderSchema>
|
package/docs/DECISIONS.adoc
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
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 ellipsis-truncates the joined line to terminal width.
|
|
8
|
-
No per-key layout, no wrap.
|
|
9
|
-
|
|
10
|
-
`ctx.ui.setFooter(factory)` is replacement-only — it swaps the whole footer (pwd, tokens, context %, model, statuses), and the factory gets `footerData` but not token/model state.
|
|
11
|
-
No "extra footer" API exists.
|
|
12
|
-
|
|
13
|
-
Decision: keep `setStatus`.
|
|
14
|
-
Revisit when the line overflows visibly with other extensions' statuses, or if the usage string grows past a typical width.
|
|
15
|
-
|
|
16
|
-
== Hand-rolled `Try<T>` instead of an effects library
|
|
17
|
-
|
|
18
|
-
Context: env loading and discovery evaluation both need a caught-error result value (`{ ok: true, value } | { ok: false, error }`).
|
|
19
|
-
Libraries like Effect ship an industrial `Either`/`Effect.try` for exactly this.
|
|
20
|
-
|
|
21
|
-
Why not: Effect is a runtime, a generator DSL, and a worldview — not a utility.
|
|
22
|
-
Two call sites in a Pi extension don't justify the dependency weight or the learning curve for contributors.
|
|
23
|
-
The hand-rolled version is ~10 lines (`Try<T>`, `runCatching`, `runCatchingAsync`, named after Kotlin's `Result`), lives in `utils.ts`, and documents intent just as well.
|
|
24
|
-
|
|
25
|
-
Decision: keep the hand-rolled `Try<T>`.
|
|
26
|
-
Revisit if the extension ever needs typed errors, retries, or resource scoping; that's the day Effect earns its place.
|
|
27
|
-
|
|
28
|
-
== Not a provider — a models.json-writing extension
|
|
29
|
-
|
|
30
|
-
Context: Requesty already works as a Pi provider via `models.json`.
|
|
31
|
-
The temptation is to make this extension *be* the provider: read the model list from the Requesty API, hold it in memory, surface usage stats, let Pi manage the API key.
|
|
32
|
-
|
|
33
|
-
Why not:
|
|
34
|
-
|
|
35
|
-
- Health-checking is the core value and has no home in a provider.
|
|
36
|
-
A provider serves models; it doesn't vet them.
|
|
37
|
-
The health check plus the diff/confirm/write flow is a discovery-and-selection step, not serving.
|
|
38
|
-
- A provider keeps its model list in-memory and refreshes occasionally.
|
|
39
|
-
Here the list is a persistent on-disk snapshot (`models.json`) — constant until you re-run
|
|
40
|
-
`/requesty-discover`.
|
|
41
|
-
That matches the intent: an explicit, reviewed, user-confirmed write, after which Pi's normal provider machinery serves the models.
|
|
42
|
-
|
|
43
|
-
The provider-like parts (API model fetch, usage footer, wanting Pi-managed auth) are real, but the health check and the snapshot model break the provider fit.
|
|
44
|
-
|
|
45
|
-
Decision: stay a `/requesty-discover` command that writes `models.json`.
|
|
46
|
-
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"type": "object",
|
|
4
|
-
"properties": {
|
|
5
|
-
"bannedModels": {
|
|
6
|
-
"default": [],
|
|
7
|
-
"type": "array",
|
|
8
|
-
"items": {
|
|
9
|
-
"type": "string",
|
|
10
|
-
"minLength": 1
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
"healthCheckMode": {
|
|
14
|
-
"default": "full",
|
|
15
|
-
"type": "string",
|
|
16
|
-
"enum": [
|
|
17
|
-
"off",
|
|
18
|
-
"basic",
|
|
19
|
-
"full"
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
"providerId": {
|
|
23
|
-
"default": "requesty-export",
|
|
24
|
-
"type": "string"
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"required": [
|
|
28
|
-
"bannedModels",
|
|
29
|
-
"healthCheckMode",
|
|
30
|
-
"providerId"
|
|
31
|
-
],
|
|
32
|
-
"additionalProperties": false,
|
|
33
|
-
"$id": "https://raw.githubusercontent.com/enolive/pi-requesty/main/docs/requesty-discovery-settings.schema.json",
|
|
34
|
-
"title": "Requesty discovery settings",
|
|
35
|
-
"description": "Settings for the pi-requesty discovery extension. The file uses JSON5, so comments are allowed."
|
|
36
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { z } from 'zod'
|
|
4
|
-
import { DiscoverySettingsSchema } from '../src/settings'
|
|
5
|
-
|
|
6
|
-
const OUT_PATH = path.resolve('docs/requesty-discovery-settings.schema.json')
|
|
7
|
-
const SCHEMA_URL =
|
|
8
|
-
'https://raw.githubusercontent.com/enolive/pi-requesty/main/docs/requesty-discovery-settings.schema.json'
|
|
9
|
-
|
|
10
|
-
const schema = z.toJSONSchema(DiscoverySettingsSchema, { target: 'draft-2020-12', io: 'output' })
|
|
11
|
-
schema.$id = SCHEMA_URL
|
|
12
|
-
schema.title = 'Requesty discovery settings'
|
|
13
|
-
schema.description = 'Settings for the pi-requesty discovery extension. The file uses JSON5, so comments are allowed.'
|
|
14
|
-
|
|
15
|
-
fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true })
|
|
16
|
-
fs.writeFileSync(OUT_PATH, `${JSON.stringify(schema, null, 2)}\n`, 'utf8')
|
|
17
|
-
console.log(`wrote ${OUT_PATH}`)
|