pi-requesty-discover 1.0.5 → 1.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 +35 -0
- package/package.json +6 -5
- package/src/env.ts +2 -8
- package/src/health-check.ts +72 -59
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
= Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing.
|
|
4
|
+
|
|
5
|
+
== Useful scripts
|
|
6
|
+
|
|
7
|
+
These will also be run in the CI/CD pipeline.
|
|
8
|
+
Make sure to run them beforehand to get faster feedback
|
|
9
|
+
about the overall quality.
|
|
10
|
+
|
|
11
|
+
[source,bash]
|
|
12
|
+
----
|
|
13
|
+
# install all dependencies
|
|
14
|
+
npm install
|
|
15
|
+
|
|
16
|
+
# linting and format checking all-in-one
|
|
17
|
+
npm run check
|
|
18
|
+
|
|
19
|
+
# run all the tests
|
|
20
|
+
npm test
|
|
21
|
+
----
|
|
22
|
+
|
|
23
|
+
Use `npm run format` to fix formatting.
|
|
24
|
+
|
|
25
|
+
Refer to additional scripts in link:package.json[].
|
|
26
|
+
|
|
27
|
+
Please keep changes focused and include tests or docs when behavior changes.
|
|
28
|
+
|
|
29
|
+
Notes:
|
|
30
|
+
|
|
31
|
+
* The package is loaded directly from TypeScript; do not add a build step casually.
|
|
32
|
+
* Keep implementation in `src/`; root `index.ts` is just the package entrypoint.
|
|
33
|
+
* Edit `README.adoc` if necessary.
|
|
34
|
+
* See `AGENTS.md` for detailed repository conventions.
|
|
35
|
+
* I am pretty good at identifying AI slop 😁.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-requesty-discover",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Discover available Requesty models for the Pi Coding Agent",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
"models"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"agent-lint
|
|
26
|
-
"
|
|
27
|
-
"format": "prettier --
|
|
25
|
+
"check": "npm run agent-lint && npm run format:check && npm run typecheck && npm run lint",
|
|
26
|
+
"agent-lint": "agnix . -v --strict",
|
|
27
|
+
"format:check": "prettier --check .",
|
|
28
|
+
"format": "prettier --write .",
|
|
28
29
|
"typecheck": "tsc --noEmit",
|
|
29
30
|
"lint": "eslint .",
|
|
30
|
-
"release": "npm version patch && git push --follow-tags",
|
|
31
|
+
"release": "read -p 'Release a new version? [y/N] ' ok && [ \"$ok\" = \"y\" ] && npm version patch && git push --follow-tags",
|
|
31
32
|
"test": "vitest run",
|
|
32
33
|
"test:unit": "vitest run src",
|
|
33
34
|
"test:unit:coverage": "vitest run src --coverage",
|
package/src/env.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import os from 'node:os'
|
|
2
1
|
import path from 'node:path'
|
|
3
2
|
import process from 'node:process'
|
|
4
3
|
import { prettifyError, z } from 'zod'
|
|
4
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent'
|
|
5
5
|
|
|
6
6
|
const HealthCheckModeSchema = z.enum(['off', 'basic', 'full']).default('full')
|
|
7
7
|
|
|
@@ -15,10 +15,6 @@ export type Env = {
|
|
|
15
15
|
|
|
16
16
|
export function getEnv(): Env {
|
|
17
17
|
const envVars = process.env
|
|
18
|
-
const configuredAgentPath = envVars.PI_CODING_AGENT_DIR
|
|
19
|
-
const defaultAgentPath = path.join(os.homedir(), '.pi', 'agent')
|
|
20
|
-
const agentPath = configuredAgentPath || defaultAgentPath
|
|
21
|
-
|
|
22
18
|
const result = HealthCheckModeSchema.safeParse(envVars.REQUESTY_HEALTH_CHECK_MODE)
|
|
23
19
|
if (!result.success) {
|
|
24
20
|
throw new Error(prettifyError(result.error))
|
|
@@ -28,7 +24,7 @@ export function getEnv(): Env {
|
|
|
28
24
|
if (!apiKey) {
|
|
29
25
|
throw new Error(`apiKey must be set via REQUESTY_API_KEY env var`)
|
|
30
26
|
}
|
|
31
|
-
|
|
27
|
+
const agentPath = getAgentDir()
|
|
32
28
|
return {
|
|
33
29
|
models_json_path: path.join(agentPath, 'models.json'),
|
|
34
30
|
health_check_log_path: path.join(agentPath, 'requesty-health-check.log'),
|
|
@@ -37,5 +33,3 @@ export function getEnv(): Env {
|
|
|
37
33
|
health_check_mode: result.data,
|
|
38
34
|
}
|
|
39
35
|
}
|
|
40
|
-
|
|
41
|
-
export default getEnv
|
package/src/health-check.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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 { z } from 'zod'
|
|
5
4
|
import { getEnv, type Env } from './env'
|
|
6
5
|
|
|
7
6
|
const HEALTH_CHECK_CONCURRENCY = 10
|
|
@@ -16,9 +15,7 @@ const defaultHealthCheckOptions = {
|
|
|
16
15
|
retryDelayMs: HEALTH_CHECK_RETRY_DELAY_MS,
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
const
|
|
20
|
-
choices: z.array(z.unknown()).min(1),
|
|
21
|
-
})
|
|
18
|
+
const DATA_PREFIX = 'data:'
|
|
22
19
|
|
|
23
20
|
export type Provider = {
|
|
24
21
|
baseUrl: string
|
|
@@ -138,48 +135,92 @@ export function writeHealthCheckLog(provider: Provider, results: HealthCheckResu
|
|
|
138
135
|
|
|
139
136
|
export async function postChatCompletion(
|
|
140
137
|
provider: Provider,
|
|
141
|
-
body: unknown
|
|
138
|
+
body: Record<string, unknown>,
|
|
142
139
|
options: HealthCheckOptions = {},
|
|
143
140
|
): Promise<ModelCheckResult> {
|
|
144
141
|
const healthCheckOptions = resolveHealthCheckOptions(options)
|
|
145
142
|
const start = Date.now()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
{
|
|
143
|
+
|
|
144
|
+
for (let attempt = 0; attempt <= healthCheckOptions.retries; attempt++) {
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(`${provider.baseUrl}/chat/completions`, {
|
|
150
147
|
method: 'POST',
|
|
151
148
|
headers: {
|
|
152
149
|
Authorization: `Bearer ${provider.apiKey}`,
|
|
153
150
|
'Content-Type': 'application/json',
|
|
151
|
+
Accept: 'text/event-stream',
|
|
154
152
|
},
|
|
155
|
-
body: JSON.stringify(body),
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
153
|
+
body: JSON.stringify({ ...body, stream: true }),
|
|
154
|
+
signal: AbortSignal.timeout(healthCheckOptions.timeoutMs),
|
|
155
|
+
})
|
|
159
156
|
|
|
160
|
-
|
|
157
|
+
const latencyMs = Date.now() - start
|
|
161
158
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const text = await response.text().catch(() => '')
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
latencyMs,
|
|
164
|
+
error: `HTTP ${response.status} ${response.statusText}${text ? `: ${text}` : ''}`,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
return await verifyFirstStreamChunk(response.body!, start)
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (isTimeoutError(err) && attempt < healthCheckOptions.retries) {
|
|
171
|
+
if (healthCheckOptions.retryDelayMs > 0) {
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, healthCheckOptions.retryDelayMs))
|
|
173
|
+
}
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
const attempts = attempt + 1
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
latencyMs: Date.now() - start,
|
|
180
|
+
error: isTimeoutError(err)
|
|
181
|
+
? `Timed out after ${attempts} attempt(s); per-attempt timeout is ${healthCheckOptions.timeoutMs / 1000}s`
|
|
182
|
+
: String(err),
|
|
183
|
+
}
|
|
170
184
|
}
|
|
185
|
+
}
|
|
171
186
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
return { ok: false, latencyMs: Date.now() - start, error: 'Unknown error' }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function verifyFirstStreamChunk(body: ReadableStream<Uint8Array>, start: number): Promise<ModelCheckResult> {
|
|
191
|
+
const reader = body.getReader()
|
|
192
|
+
const decoder = new TextDecoder()
|
|
193
|
+
let tail = ''
|
|
194
|
+
try {
|
|
195
|
+
while (true) {
|
|
196
|
+
const { done, value } = await reader.read()
|
|
197
|
+
if (done) {
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
tail += decoder.decode(value, { stream: true })
|
|
201
|
+
const lines = tail.split('\n')
|
|
202
|
+
tail = lines.pop()!
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
if (!line.startsWith(DATA_PREFIX)) {
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
const payload = line.slice(DATA_PREFIX.length).trim()
|
|
208
|
+
if (payload === '[DONE]') {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(payload) as { choices?: unknown }
|
|
213
|
+
if (Array.isArray(parsed.choices) && parsed.choices.length > 0) {
|
|
214
|
+
return { ok: true, latencyMs: Date.now() - start }
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// partial JSON or non-choices chunk — keep reading
|
|
218
|
+
}
|
|
219
|
+
}
|
|
182
220
|
}
|
|
221
|
+
return { ok: false, latencyMs: Date.now() - start, error: 'Stream ended without content' }
|
|
222
|
+
} finally {
|
|
223
|
+
await reader.cancel()
|
|
183
224
|
}
|
|
184
225
|
}
|
|
185
226
|
|
|
@@ -248,34 +289,6 @@ function resolveHealthCheckOptions(options: HealthCheckOptions): ResolvedHealthC
|
|
|
248
289
|
}
|
|
249
290
|
}
|
|
250
291
|
|
|
251
|
-
async function fetchWithTimeoutRetries(
|
|
252
|
-
url: string,
|
|
253
|
-
options: RequestInit,
|
|
254
|
-
{ timeoutMs, retries, retryDelayMs }: ResolvedHealthCheckOptions,
|
|
255
|
-
) {
|
|
256
|
-
function sleep(ms: number) {
|
|
257
|
-
if (ms <= 0) {
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
for (let attempt = 0; ; attempt++) {
|
|
264
|
-
try {
|
|
265
|
-
return await fetch(url, {
|
|
266
|
-
...options,
|
|
267
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
268
|
-
})
|
|
269
|
-
} catch (error) {
|
|
270
|
-
if (!isTimeoutError(error) || attempt >= retries) {
|
|
271
|
-
throw error
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
await sleep(retryDelayMs)
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
292
|
function isTimeoutError(error: unknown): boolean {
|
|
280
293
|
return error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')
|
|
281
294
|
}
|