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.
- package/package.json +1 -1
- package/src/health-check.ts +72 -59
package/package.json
CHANGED
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
|
}
|