pi-requesty-discover 1.0.6 → 1.2.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 +81 -60
- package/src/index.ts +13 -4
- package/src/models-json.ts +26 -0
package/package.json
CHANGED
package/src/health-check.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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'
|
|
5
|
+
import { formatModelsDiffSummary, type ModelsDiff } from './models-json'
|
|
6
6
|
|
|
7
7
|
const HEALTH_CHECK_CONCURRENCY = 10
|
|
8
8
|
const HEALTH_CHECK_TIMEOUT_MS = 15_000
|
|
@@ -16,9 +16,7 @@ const defaultHealthCheckOptions = {
|
|
|
16
16
|
retryDelayMs: HEALTH_CHECK_RETRY_DELAY_MS,
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
choices: z.array(z.unknown()).min(1),
|
|
21
|
-
})
|
|
19
|
+
const DATA_PREFIX = 'data:'
|
|
22
20
|
|
|
23
21
|
export type Provider = {
|
|
24
22
|
baseUrl: string
|
|
@@ -101,7 +99,12 @@ export function formatHealthSummary(results: HealthCheckResult[]): string {
|
|
|
101
99
|
return `Health check: ${passed.length} OK, ${failed.length} failed:\n${failedModels}\n`
|
|
102
100
|
}
|
|
103
101
|
|
|
104
|
-
export function writeHealthCheckLog(
|
|
102
|
+
export function writeHealthCheckLog(
|
|
103
|
+
provider: Provider,
|
|
104
|
+
results: HealthCheckResult[],
|
|
105
|
+
diff: ModelsDiff,
|
|
106
|
+
envConfig: Env = getEnv(),
|
|
107
|
+
): void {
|
|
105
108
|
const passed = results.filter(r => r.ok)
|
|
106
109
|
const failed = results.filter(r => !r.ok)
|
|
107
110
|
const lines = [
|
|
@@ -113,6 +116,8 @@ export function writeHealthCheckLog(provider: Provider, results: HealthCheckResu
|
|
|
113
116
|
`Passed: ${passed.length}`,
|
|
114
117
|
`Failed: ${failed.length}`,
|
|
115
118
|
'',
|
|
119
|
+
formatModelsDiffSummary(diff),
|
|
120
|
+
'',
|
|
116
121
|
]
|
|
117
122
|
|
|
118
123
|
if (failed.length === 0) {
|
|
@@ -138,48 +143,92 @@ export function writeHealthCheckLog(provider: Provider, results: HealthCheckResu
|
|
|
138
143
|
|
|
139
144
|
export async function postChatCompletion(
|
|
140
145
|
provider: Provider,
|
|
141
|
-
body: unknown
|
|
146
|
+
body: Record<string, unknown>,
|
|
142
147
|
options: HealthCheckOptions = {},
|
|
143
148
|
): Promise<ModelCheckResult> {
|
|
144
149
|
const healthCheckOptions = resolveHealthCheckOptions(options)
|
|
145
150
|
const start = Date.now()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
{
|
|
151
|
+
|
|
152
|
+
for (let attempt = 0; attempt <= healthCheckOptions.retries; attempt++) {
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(`${provider.baseUrl}/chat/completions`, {
|
|
150
155
|
method: 'POST',
|
|
151
156
|
headers: {
|
|
152
157
|
Authorization: `Bearer ${provider.apiKey}`,
|
|
153
158
|
'Content-Type': 'application/json',
|
|
159
|
+
Accept: 'text/event-stream',
|
|
154
160
|
},
|
|
155
|
-
body: JSON.stringify(body),
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
161
|
+
body: JSON.stringify({ ...body, stream: true }),
|
|
162
|
+
signal: AbortSignal.timeout(healthCheckOptions.timeoutMs),
|
|
163
|
+
})
|
|
159
164
|
|
|
160
|
-
|
|
165
|
+
const latencyMs = Date.now() - start
|
|
161
166
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
const text = await response.text().catch(() => '')
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
latencyMs,
|
|
172
|
+
error: `HTTP ${response.status} ${response.statusText}${text ? `: ${text}` : ''}`,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
166
175
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
return await verifyFirstStreamChunk(response.body!, start)
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (isTimeoutError(err) && attempt < healthCheckOptions.retries) {
|
|
179
|
+
if (healthCheckOptions.retryDelayMs > 0) {
|
|
180
|
+
await new Promise(resolve => setTimeout(resolve, healthCheckOptions.retryDelayMs))
|
|
181
|
+
}
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
const attempts = attempt + 1
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
latencyMs: Date.now() - start,
|
|
188
|
+
error: isTimeoutError(err)
|
|
189
|
+
? `Timed out after ${attempts} attempt(s); per-attempt timeout is ${healthCheckOptions.timeoutMs / 1000}s`
|
|
190
|
+
: String(err),
|
|
191
|
+
}
|
|
170
192
|
}
|
|
193
|
+
}
|
|
171
194
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
195
|
+
return { ok: false, latencyMs: Date.now() - start, error: 'Unknown error' }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function verifyFirstStreamChunk(body: ReadableStream<Uint8Array>, start: number): Promise<ModelCheckResult> {
|
|
199
|
+
const reader = body.getReader()
|
|
200
|
+
const decoder = new TextDecoder()
|
|
201
|
+
let tail = ''
|
|
202
|
+
try {
|
|
203
|
+
while (true) {
|
|
204
|
+
const { done, value } = await reader.read()
|
|
205
|
+
if (done) {
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
tail += decoder.decode(value, { stream: true })
|
|
209
|
+
const lines = tail.split('\n')
|
|
210
|
+
tail = lines.pop()!
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
if (!line.startsWith(DATA_PREFIX)) {
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
const payload = line.slice(DATA_PREFIX.length).trim()
|
|
216
|
+
if (payload === '[DONE]') {
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(payload) as { choices?: unknown }
|
|
221
|
+
if (Array.isArray(parsed.choices) && parsed.choices.length > 0) {
|
|
222
|
+
return { ok: true, latencyMs: Date.now() - start }
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// partial JSON or non-choices chunk — keep reading
|
|
226
|
+
}
|
|
227
|
+
}
|
|
182
228
|
}
|
|
229
|
+
return { ok: false, latencyMs: Date.now() - start, error: 'Stream ended without content' }
|
|
230
|
+
} finally {
|
|
231
|
+
await reader.cancel()
|
|
183
232
|
}
|
|
184
233
|
}
|
|
185
234
|
|
|
@@ -248,34 +297,6 @@ function resolveHealthCheckOptions(options: HealthCheckOptions): ResolvedHealthC
|
|
|
248
297
|
}
|
|
249
298
|
}
|
|
250
299
|
|
|
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
300
|
function isTimeoutError(error: unknown): boolean {
|
|
280
301
|
return error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')
|
|
281
302
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { ExtensionAPI, ExtensionCommandContext, ProviderModelConfig } from '@earendil-works/pi-coding-agent'
|
|
2
2
|
import { getEnv } from './env'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
diffModels,
|
|
5
|
+
formatModelsDiffSummary,
|
|
6
|
+
getRequestyConfig,
|
|
7
|
+
updateModelsJson,
|
|
8
|
+
type ModelsDiff,
|
|
9
|
+
} from './models-json'
|
|
4
10
|
import { discoverModels } from './requesty-api'
|
|
5
11
|
import { checkModels, formatHealthSummary, writeHealthCheckLog } from './health-check'
|
|
6
12
|
import { RequestyStatusLoader } from './ui/requesty-status-loader.ts'
|
|
@@ -67,10 +73,11 @@ export async function runCommand(args: string, status: StatusReporter, notifier:
|
|
|
67
73
|
|
|
68
74
|
try {
|
|
69
75
|
const env = getEnv()
|
|
70
|
-
const { data, provider } = getRequestyConfig(env)
|
|
76
|
+
const { data, provider, existingModelIds } = getRequestyConfig(env)
|
|
71
77
|
const models = await discoverModels(provider)
|
|
72
78
|
const modelsMap = new Map(models.map(m => [m.id, m]))
|
|
73
79
|
|
|
80
|
+
let diff: ModelsDiff
|
|
74
81
|
let failed = []
|
|
75
82
|
let passing: ProviderModelConfig[]
|
|
76
83
|
let logNote = ''
|
|
@@ -89,11 +96,13 @@ export async function runCommand(args: string, status: StatusReporter, notifier:
|
|
|
89
96
|
const model = modelsMap.get(r.modelId)
|
|
90
97
|
return r.ok && model ? [model] : []
|
|
91
98
|
})
|
|
99
|
+
diff = diffModels(existingModelIds, passing)
|
|
92
100
|
healthCheckSummary = formatHealthSummary(sortedResults)
|
|
93
|
-
writeHealthCheckLog(provider, sortedResults, env)
|
|
101
|
+
writeHealthCheckLog(provider, sortedResults, diff, env)
|
|
94
102
|
logNote = `Full health check log: ${env.health_check_log_path}\n`
|
|
95
103
|
} else {
|
|
96
104
|
passing = models
|
|
105
|
+
diff = diffModels(existingModelIds, passing)
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
const shouldUpdate = passing.length > 0 && !dryRun
|
|
@@ -102,7 +111,7 @@ export async function runCommand(args: string, status: StatusReporter, notifier:
|
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
const writeNote = shouldUpdate ? 'Run /reload to use models.json changes.' : 'models.json was not updated.'
|
|
105
|
-
const message = `Discovered ${models.length} Requesty model(s).\n${healthCheckSummary}${logNote}${writeNote}`
|
|
114
|
+
const message = `Discovered ${models.length} Requesty model(s).\n${healthCheckSummary}${formatModelsDiffSummary(diff)}\n${logNote}${writeNote}`
|
|
106
115
|
|
|
107
116
|
if (failed.length === 0) {
|
|
108
117
|
notifier.notify(message, 'info')
|
package/src/models-json.ts
CHANGED
|
@@ -12,6 +12,7 @@ const ProviderSchema = z
|
|
|
12
12
|
name: z.string().optional(),
|
|
13
13
|
baseUrl: z.string().optional(),
|
|
14
14
|
apiKey: z.string().optional(),
|
|
15
|
+
models: z.array(z.object({ id: z.string() }).catchall(z.unknown())).optional(),
|
|
15
16
|
})
|
|
16
17
|
.catchall(z.unknown())
|
|
17
18
|
|
|
@@ -33,6 +34,12 @@ type RequestyProvider = ModelsJsonProvider & {
|
|
|
33
34
|
type RequestyConfig = {
|
|
34
35
|
data: ModelsJson
|
|
35
36
|
provider: RequestyProvider
|
|
37
|
+
existingModelIds: string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ModelsDiff = {
|
|
41
|
+
added: string[]
|
|
42
|
+
removed: string[]
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
export function getRequestyConfig(envConfig: Env = getEnv()): RequestyConfig {
|
|
@@ -46,6 +53,7 @@ export function getRequestyConfig(envConfig: Env = getEnv()): RequestyConfig {
|
|
|
46
53
|
const apiKey = envConfig.requesty_api_key
|
|
47
54
|
return {
|
|
48
55
|
data,
|
|
56
|
+
existingModelIds: (provider.models ?? []).map(m => m.id),
|
|
49
57
|
provider: {
|
|
50
58
|
...provider,
|
|
51
59
|
name: nonEmptyString(provider.name) ?? DEFAULT_NAME,
|
|
@@ -55,6 +63,24 @@ export function getRequestyConfig(envConfig: Env = getEnv()): RequestyConfig {
|
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
export function diffModels(previousIds: string[], nextModels: ProviderModelConfig[]): ModelsDiff {
|
|
67
|
+
const previous = new Set(previousIds)
|
|
68
|
+
const next = new Set(nextModels.map(m => m.id))
|
|
69
|
+
return {
|
|
70
|
+
added: [...next].filter(id => !previous.has(id)).toSorted(),
|
|
71
|
+
removed: [...previous].filter(id => !next.has(id)).toSorted(),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatModelsDiffSummary(diff: ModelsDiff): string {
|
|
76
|
+
return [
|
|
77
|
+
diff.added.length === 0 ? 'No added models.' : 'Added models:',
|
|
78
|
+
...diff.added.map(id => `- ${id}`),
|
|
79
|
+
diff.removed.length === 0 ? 'No removed models.' : 'Removed models:',
|
|
80
|
+
...diff.removed.map(id => `- ${id}`),
|
|
81
|
+
].join('\n')
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
export function updateModelsJson(data: ModelsJson, models: ProviderModelConfig[], envConfig: Env = getEnv()): void {
|
|
59
85
|
const provider = data.providers[envConfig.provider_id]
|
|
60
86
|
data.providers[envConfig.provider_id] = {
|