pi-requesty-discover 1.0.6 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/health-check.ts +72 -59
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-requesty-discover",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "Discover available Requesty models for the Pi Coding Agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
  }