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.
@@ -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.5",
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": "agnix . -v",
26
- "format:check": "prettier --check src test",
27
- "format": "prettier --write src test",
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
@@ -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 ChatCompletionResponseSchema = z.object({
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
- try {
147
- const response = await fetchWithTimeoutRetries(
148
- `${provider.baseUrl}/chat/completions`,
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
- healthCheckOptions,
158
- )
153
+ body: JSON.stringify({ ...body, stream: true }),
154
+ signal: AbortSignal.timeout(healthCheckOptions.timeoutMs),
155
+ })
159
156
 
160
- const latencyMs = Date.now() - start
157
+ const latencyMs = Date.now() - start
161
158
 
162
- if (!response.ok) {
163
- const text = await response.text().catch(() => '')
164
- return { ok: false, latencyMs, error: `HTTP ${response.status} ${response.statusText}${text ? `: ${text}` : ''}` }
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
- const result = ChatCompletionResponseSchema.safeParse(await response.json())
168
- if (!result.success) {
169
- return { ok: false, latencyMs, error: 'Empty choices in response' }
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
- return { ok: true, latencyMs }
173
- } catch (err) {
174
- const latencyMs = Date.now() - start
175
- const attempts = healthCheckOptions.retries + 1
176
- return {
177
- ok: false,
178
- latencyMs,
179
- error: isTimeoutError(err)
180
- ? `Timed out after ${attempts} attempt(s); per-attempt timeout is ${healthCheckOptions.timeoutMs / 1000}s`
181
- : String(err),
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
  }