impel-cli 0.7.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/README.md +695 -0
- package/bin/impel.js +7 -0
- package/package.json +29 -0
- package/src/apps.js +1263 -0
- package/src/args.js +36 -0
- package/src/claudeSetup.js +207 -0
- package/src/cli.js +184 -0
- package/src/cliProfiles.js +216 -0
- package/src/codexSecurity.js +184 -0
- package/src/codexSetup.js +224 -0
- package/src/commands/apps.js +538 -0
- package/src/commands/auth.js +89 -0
- package/src/commands/doctor.js +215 -0
- package/src/commands/experimental.js +60 -0
- package/src/commands/launch.js +161 -0
- package/src/commands/mcp.js +94 -0
- package/src/commands/setup.js +350 -0
- package/src/commands/skills.js +108 -0
- package/src/commands/status.js +95 -0
- package/src/commands/tasks.js +359 -0
- package/src/commands/tenant.js +77 -0
- package/src/commands/token.js +25 -0
- package/src/commands/update.js +217 -0
- package/src/commands/use.js +208 -0
- package/src/config.js +98 -0
- package/src/doctor.js +546 -0
- package/src/nativeProcess.js +192 -0
- package/src/prompt.js +51 -0
- package/src/selfInvocation.js +21 -0
- package/src/skills.js +314 -0
- package/src/tenants.js +194 -0
- package/src/updates.js +181 -0
- package/src/windowsApps.js +439 -0
- package/src/windowsSetup.js +122 -0
package/src/doctor.js
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { normalizeGatewayUrl, redactSecretText, resolveDefaultGateway } from "./config.js";
|
|
4
|
+
import { tenantCredential } from "./tenants.js";
|
|
5
|
+
|
|
6
|
+
export const DOCTOR_PROVIDERS = ["claude", "codex"];
|
|
7
|
+
export const DEFAULT_TTFT_BUDGET_MS = Object.freeze({ claude: 6_000, codex: 5_000 });
|
|
8
|
+
|
|
9
|
+
const MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
10
|
+
const MAX_OUTPUT_CHARS = 16 * 1024;
|
|
11
|
+
const MAX_PENDING_SSE_CHARS = 1024 * 1024;
|
|
12
|
+
const MAX_STREAM_BYTES = 4 * 1024 * 1024;
|
|
13
|
+
const GATEWAY_REQUEST_ID_RE = /^impel-[a-f0-9]{32}$/u;
|
|
14
|
+
const POOL_STATES = new Set(["healthy", "exhausted", "rate_limited", "expired", "no_seat"]);
|
|
15
|
+
|
|
16
|
+
function round(value) {
|
|
17
|
+
return Number.isFinite(value) ? Math.round(value) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function percentile(values, proportion) {
|
|
21
|
+
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
|
|
22
|
+
if (!sorted.length) return null;
|
|
23
|
+
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * proportion) - 1)];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeErrorMessage(payload, fallback) {
|
|
27
|
+
const candidate = payload?.error?.message || payload?.error || payload?.message || fallback;
|
|
28
|
+
const text = typeof candidate === "string" ? candidate : JSON.stringify(candidate);
|
|
29
|
+
return redactSecretText(String(text || fallback).replace(/\s+/gu, " ")).slice(0, 300);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readTextLimited(response, limit = MAX_ERROR_BODY_BYTES) {
|
|
33
|
+
if (!response.body) return "";
|
|
34
|
+
const reader = response.body.getReader();
|
|
35
|
+
const decoder = new TextDecoder();
|
|
36
|
+
let text = "";
|
|
37
|
+
let bytes = 0;
|
|
38
|
+
while (bytes < limit) {
|
|
39
|
+
const { done, value } = await reader.read();
|
|
40
|
+
if (done) break;
|
|
41
|
+
bytes += value.byteLength;
|
|
42
|
+
text += decoder.decode(value, { stream: true });
|
|
43
|
+
if (bytes >= limit) {
|
|
44
|
+
await reader.cancel().catch(() => {});
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
text += decoder.decode();
|
|
49
|
+
return text.slice(0, limit);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readError(response) {
|
|
53
|
+
const text = await readTextLimited(response);
|
|
54
|
+
let payload = null;
|
|
55
|
+
try {
|
|
56
|
+
payload = text ? JSON.parse(text) : null;
|
|
57
|
+
} catch {
|
|
58
|
+
// Do not echo arbitrary upstream response bodies into terminal output.
|
|
59
|
+
}
|
|
60
|
+
return safeErrorMessage(payload, `${response.status} ${response.statusText || "gateway error"}`.trim());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function consumeSseBlock(provider, block, state, startedAt, now) {
|
|
64
|
+
const data = block
|
|
65
|
+
.split(/\r\n|\r|\n/gu)
|
|
66
|
+
.filter((line) => line.startsWith("data:"))
|
|
67
|
+
.map((line) => line.slice(5).trimStart())
|
|
68
|
+
.join("\n")
|
|
69
|
+
.trim();
|
|
70
|
+
if (!data) return;
|
|
71
|
+
if (data === "[DONE]") {
|
|
72
|
+
state.doneSentinel = true;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let event;
|
|
77
|
+
try {
|
|
78
|
+
event = JSON.parse(data);
|
|
79
|
+
} catch {
|
|
80
|
+
state.error ||= "gateway returned malformed JSON in the SSE stream";
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let delta = "";
|
|
85
|
+
if (provider === "claude") {
|
|
86
|
+
if (!state.responseId && typeof event?.message?.id === "string") {
|
|
87
|
+
state.responseId = event.message.id.slice(0, 512) || null;
|
|
88
|
+
}
|
|
89
|
+
if (event?.type === "message_stop") state.terminalEvent = event.type;
|
|
90
|
+
if (event?.type === "content_block_delta" && event?.delta?.type === "text_delta") {
|
|
91
|
+
delta = typeof event.delta.text === "string" ? event.delta.text : "";
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
if (!state.responseId && typeof event?.response?.id === "string") {
|
|
95
|
+
state.responseId = event.response.id.slice(0, 512) || null;
|
|
96
|
+
}
|
|
97
|
+
if (event?.type === "response.completed") {
|
|
98
|
+
if (event?.response?.status && event.response.status !== "completed") {
|
|
99
|
+
state.error ||= `provider completed the response with status ${redactSecretText(event.response.status)}`;
|
|
100
|
+
} else {
|
|
101
|
+
state.terminalEvent = event.type;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (["response.failed", "response.incomplete", "response.cancelled"].includes(event?.type)) {
|
|
105
|
+
state.error ||= safeErrorMessage(event?.response || event, `provider emitted ${event.type}`);
|
|
106
|
+
}
|
|
107
|
+
if (event?.type === "response.output_text.delta") {
|
|
108
|
+
delta = typeof event.delta === "string" ? event.delta : "";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (event?.type === "error") {
|
|
112
|
+
state.error ||= safeErrorMessage(event, "provider emitted an SSE error event");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!delta) return;
|
|
116
|
+
state.ttftMs ??= now() - startedAt;
|
|
117
|
+
if (state.answer.length < MAX_OUTPUT_CHARS) {
|
|
118
|
+
const remaining = MAX_OUTPUT_CHARS - state.answer.length;
|
|
119
|
+
state.answer += delta.slice(0, remaining);
|
|
120
|
+
if (delta.length > remaining) state.outputTruncated = true;
|
|
121
|
+
} else {
|
|
122
|
+
state.outputTruncated = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function streamProbe({ provider, url, bearer, model, tenantId, attempt, timeoutMs, fetchImpl, uuid, now }) {
|
|
127
|
+
// Keep customer/org names out of the provider-visible synthetic prompt.
|
|
128
|
+
const clientRequestId = `impel-doctor-${provider}-${attempt}-${uuid()}`;
|
|
129
|
+
const expected = `ACK ${clientRequestId}`;
|
|
130
|
+
const prompt = `Synthetic Impel production routing check ${clientRequestId}. Reply with exactly: ${expected}`;
|
|
131
|
+
const body = provider === "claude"
|
|
132
|
+
? {
|
|
133
|
+
model,
|
|
134
|
+
max_tokens: 96,
|
|
135
|
+
stream: true,
|
|
136
|
+
messages: [{ role: "user", content: prompt }],
|
|
137
|
+
}
|
|
138
|
+
: {
|
|
139
|
+
model,
|
|
140
|
+
input: [{ role: "user", content: [{ type: "input_text", text: prompt }] }],
|
|
141
|
+
store: false,
|
|
142
|
+
stream: true,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
147
|
+
const startedAt = now();
|
|
148
|
+
let responseMetadata = {
|
|
149
|
+
requestId: null,
|
|
150
|
+
echoedClientRequestId: null,
|
|
151
|
+
accountId: null,
|
|
152
|
+
orgId: null,
|
|
153
|
+
status: null,
|
|
154
|
+
headersMs: null,
|
|
155
|
+
contentType: "",
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetchImpl(url, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
accept: "text/event-stream",
|
|
162
|
+
authorization: `Bearer ${bearer}`,
|
|
163
|
+
"content-type": "application/json",
|
|
164
|
+
"x-request-id": clientRequestId,
|
|
165
|
+
...(provider === "claude" ? { "anthropic-version": "2023-06-01" } : {}),
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify(body),
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
});
|
|
170
|
+
const headersMs = now() - startedAt;
|
|
171
|
+
responseMetadata = {
|
|
172
|
+
requestId: response.headers.get("x-impel-request-id") || null,
|
|
173
|
+
echoedClientRequestId: response.headers.get("x-impel-client-request-id") || null,
|
|
174
|
+
accountId: response.headers.get("x-impel-account-id") || null,
|
|
175
|
+
orgId: response.headers.get("x-impel-org-id") || null,
|
|
176
|
+
status: response.status,
|
|
177
|
+
headersMs: round(headersMs),
|
|
178
|
+
contentType: response.headers.get("content-type") || "",
|
|
179
|
+
};
|
|
180
|
+
if (!response.ok || !response.body) {
|
|
181
|
+
return {
|
|
182
|
+
tenantId,
|
|
183
|
+
provider,
|
|
184
|
+
attempt,
|
|
185
|
+
model,
|
|
186
|
+
...responseMetadata,
|
|
187
|
+
clientRequestId,
|
|
188
|
+
responseId: null,
|
|
189
|
+
firstByteMs: round(headersMs),
|
|
190
|
+
ttftMs: null,
|
|
191
|
+
totalMs: round(now() - startedAt),
|
|
192
|
+
exactAck: false,
|
|
193
|
+
outputChars: 0,
|
|
194
|
+
outputTruncated: false,
|
|
195
|
+
terminalEvent: null,
|
|
196
|
+
doneSentinel: false,
|
|
197
|
+
receivedBytes: 0,
|
|
198
|
+
error: await readError(response),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const state = {
|
|
203
|
+
answer: "",
|
|
204
|
+
responseId: null,
|
|
205
|
+
ttftMs: null,
|
|
206
|
+
outputTruncated: false,
|
|
207
|
+
terminalEvent: null,
|
|
208
|
+
doneSentinel: false,
|
|
209
|
+
error: null,
|
|
210
|
+
};
|
|
211
|
+
const reader = response.body.getReader();
|
|
212
|
+
const decoder = new TextDecoder();
|
|
213
|
+
let pending = "";
|
|
214
|
+
let firstByteMs = null;
|
|
215
|
+
let receivedBytes = 0;
|
|
216
|
+
while (true) {
|
|
217
|
+
const { done, value } = await reader.read();
|
|
218
|
+
if (done) break;
|
|
219
|
+
firstByteMs ??= now() - startedAt;
|
|
220
|
+
receivedBytes += value.byteLength;
|
|
221
|
+
if (receivedBytes > MAX_STREAM_BYTES) {
|
|
222
|
+
await reader.cancel().catch(() => {});
|
|
223
|
+
throw new Error("gateway response exceeded the production probe stream limit");
|
|
224
|
+
}
|
|
225
|
+
pending += decoder.decode(value, { stream: true });
|
|
226
|
+
if (pending.length > MAX_PENDING_SSE_CHARS) {
|
|
227
|
+
await reader.cancel().catch(() => {});
|
|
228
|
+
throw new Error("gateway returned an invalid oversized SSE event");
|
|
229
|
+
}
|
|
230
|
+
const blocks = pending.split(/(?:(?:\r\n|\r|\n)){2}/gu);
|
|
231
|
+
pending = blocks.pop() || "";
|
|
232
|
+
for (const block of blocks) consumeSseBlock(provider, block, state, startedAt, now);
|
|
233
|
+
}
|
|
234
|
+
pending += decoder.decode();
|
|
235
|
+
if (pending) consumeSseBlock(provider, pending, state, startedAt, now);
|
|
236
|
+
|
|
237
|
+
const exactAck = state.answer.trim() === expected;
|
|
238
|
+
const validationError = state.error
|
|
239
|
+
|| (!state.responseId ? "provider stream did not include a response id" : null)
|
|
240
|
+
|| (!state.terminalEvent ? "provider stream ended without a successful terminal event" : null)
|
|
241
|
+
|| (!GATEWAY_REQUEST_ID_RE.test(responseMetadata.requestId || "")
|
|
242
|
+
? "response did not include a valid gateway-owned request id"
|
|
243
|
+
: null)
|
|
244
|
+
|| (responseMetadata.requestId === clientRequestId
|
|
245
|
+
? "gateway reused the caller correlation id as its request id"
|
|
246
|
+
: null)
|
|
247
|
+
|| (responseMetadata.echoedClientRequestId !== clientRequestId
|
|
248
|
+
? "response did not echo the caller correlation id"
|
|
249
|
+
: null);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
tenantId,
|
|
253
|
+
provider,
|
|
254
|
+
attempt,
|
|
255
|
+
model,
|
|
256
|
+
...responseMetadata,
|
|
257
|
+
clientRequestId,
|
|
258
|
+
responseId: state.responseId,
|
|
259
|
+
firstByteMs: round(firstByteMs ?? headersMs),
|
|
260
|
+
ttftMs: round(state.ttftMs),
|
|
261
|
+
totalMs: round(now() - startedAt),
|
|
262
|
+
exactAck,
|
|
263
|
+
outputChars: state.answer.length,
|
|
264
|
+
outputTruncated: state.outputTruncated,
|
|
265
|
+
terminalEvent: state.terminalEvent,
|
|
266
|
+
doneSentinel: state.doneSentinel,
|
|
267
|
+
receivedBytes,
|
|
268
|
+
error: validationError,
|
|
269
|
+
};
|
|
270
|
+
} catch (error) {
|
|
271
|
+
const timedOut = error?.name === "AbortError";
|
|
272
|
+
return {
|
|
273
|
+
tenantId,
|
|
274
|
+
provider,
|
|
275
|
+
attempt,
|
|
276
|
+
model,
|
|
277
|
+
...responseMetadata,
|
|
278
|
+
clientRequestId,
|
|
279
|
+
responseId: null,
|
|
280
|
+
firstByteMs: null,
|
|
281
|
+
ttftMs: null,
|
|
282
|
+
totalMs: round(now() - startedAt),
|
|
283
|
+
exactAck: false,
|
|
284
|
+
outputChars: 0,
|
|
285
|
+
outputTruncated: false,
|
|
286
|
+
terminalEvent: null,
|
|
287
|
+
doneSentinel: false,
|
|
288
|
+
receivedBytes: 0,
|
|
289
|
+
error: timedOut
|
|
290
|
+
? `timed out after ${timeoutMs}ms`
|
|
291
|
+
: redactSecretText(error?.message || error).slice(0, 300),
|
|
292
|
+
};
|
|
293
|
+
} finally {
|
|
294
|
+
clearTimeout(timeout);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function modelReady(model) {
|
|
299
|
+
return model?.available !== false && model?.ready !== false && model?.readiness?.ready !== false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function providerCatalogStatuses(payload) {
|
|
303
|
+
if (!payload || typeof payload !== "object" || !Object.hasOwn(payload, "provider_status")) {
|
|
304
|
+
return { statuses: {}, malformed: false };
|
|
305
|
+
}
|
|
306
|
+
const rawStatuses = payload.provider_status;
|
|
307
|
+
if (!rawStatuses || typeof rawStatuses !== "object" || Array.isArray(rawStatuses)) {
|
|
308
|
+
return { statuses: {}, malformed: true };
|
|
309
|
+
}
|
|
310
|
+
const statuses = {};
|
|
311
|
+
for (const provider of DOCTOR_PROVIDERS) {
|
|
312
|
+
if (!Object.hasOwn(rawStatuses, provider)) continue;
|
|
313
|
+
const raw = rawStatuses[provider];
|
|
314
|
+
if (!raw
|
|
315
|
+
|| typeof raw !== "object"
|
|
316
|
+
|| Array.isArray(raw)
|
|
317
|
+
|| !POOL_STATES.has(raw.state)
|
|
318
|
+
|| typeof raw.routable !== "boolean") {
|
|
319
|
+
return { statuses: {}, malformed: true };
|
|
320
|
+
}
|
|
321
|
+
statuses[provider] = { state: raw.state, routable: raw.routable };
|
|
322
|
+
}
|
|
323
|
+
return { statuses, malformed: false };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function recordSucceeded(record) {
|
|
327
|
+
return record.status === 200
|
|
328
|
+
&& !record.error
|
|
329
|
+
&& record.exactAck
|
|
330
|
+
&& Boolean(record.responseId)
|
|
331
|
+
&& Boolean(record.terminalEvent)
|
|
332
|
+
&& GATEWAY_REQUEST_ID_RE.test(record.requestId || "")
|
|
333
|
+
&& record.requestId !== record.clientRequestId
|
|
334
|
+
&& record.echoedClientRequestId === record.clientRequestId
|
|
335
|
+
&& Boolean(record.accountId)
|
|
336
|
+
&& record.orgId === record.tenantId
|
|
337
|
+
&& record.contentType.toLowerCase().includes("text/event-stream");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function selectModel(models, provider) {
|
|
341
|
+
const candidates = models.filter((model) => model?.provider === provider && modelReady(model));
|
|
342
|
+
return candidates.find((model) => model.default)?.id || candidates[0]?.id || null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function summarizeProvider(records, provider, ttftBudgetMs, catalogStatus, skipped) {
|
|
346
|
+
const selected = records.filter((record) => record.provider === provider);
|
|
347
|
+
const successful = selected.filter(recordSucceeded);
|
|
348
|
+
const withinBudget = successful.filter((record) => record.ttftMs !== null && record.ttftMs <= ttftBudgetMs);
|
|
349
|
+
return {
|
|
350
|
+
catalogState: catalogStatus?.state || null,
|
|
351
|
+
routable: catalogStatus?.routable ?? null,
|
|
352
|
+
skipped,
|
|
353
|
+
attempted: selected.length,
|
|
354
|
+
successful: successful.length,
|
|
355
|
+
withinLatencyBudget: withinBudget.length,
|
|
356
|
+
ttftBudgetMs,
|
|
357
|
+
ttftP50Ms: percentile(successful.map((record) => record.ttftMs), 0.5),
|
|
358
|
+
ttftP95Ms: percentile(successful.map((record) => record.ttftMs), 0.95),
|
|
359
|
+
totalP50Ms: percentile(successful.map((record) => record.totalMs), 0.5),
|
|
360
|
+
totalP95Ms: percentile(successful.map((record) => record.totalMs), 0.95),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function probeTenant({
|
|
365
|
+
config,
|
|
366
|
+
tenantId,
|
|
367
|
+
productAccess,
|
|
368
|
+
providers = DOCTOR_PROVIDERS,
|
|
369
|
+
attempts = 1,
|
|
370
|
+
timeoutMs = 120_000,
|
|
371
|
+
ttftBudgets = DEFAULT_TTFT_BUDGET_MS,
|
|
372
|
+
strictLatency = false,
|
|
373
|
+
fetchImpl = fetch,
|
|
374
|
+
uuid = randomUUID,
|
|
375
|
+
now = () => performance.now(),
|
|
376
|
+
}) {
|
|
377
|
+
const gatewayUrl = normalizeGatewayUrl(config.gatewayUrl || resolveDefaultGateway());
|
|
378
|
+
const bearer = tenantCredential(config.pat, tenantId);
|
|
379
|
+
const catalogStartedAt = now();
|
|
380
|
+
const catalogController = new AbortController();
|
|
381
|
+
const catalogTimeoutMs = Math.min(timeoutMs, 30_000);
|
|
382
|
+
const catalogTimeout = setTimeout(() => catalogController.abort(), catalogTimeoutMs);
|
|
383
|
+
let response;
|
|
384
|
+
let catalogText;
|
|
385
|
+
try {
|
|
386
|
+
response = await fetchImpl(new URL("/v1/models", gatewayUrl), {
|
|
387
|
+
headers: { accept: "application/json", authorization: `Bearer ${bearer}` },
|
|
388
|
+
signal: catalogController.signal,
|
|
389
|
+
});
|
|
390
|
+
catalogText = await readTextLimited(response);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
const timedOut = error?.name === "AbortError";
|
|
393
|
+
return {
|
|
394
|
+
tenantId,
|
|
395
|
+
productAccess,
|
|
396
|
+
catalog: {
|
|
397
|
+
status: null,
|
|
398
|
+
durationMs: round(now() - catalogStartedAt),
|
|
399
|
+
models: 0,
|
|
400
|
+
error: timedOut
|
|
401
|
+
? `model catalog timed out after ${catalogTimeoutMs}ms`
|
|
402
|
+
: redactSecretText(error?.message || error).slice(0, 300),
|
|
403
|
+
},
|
|
404
|
+
records: [],
|
|
405
|
+
providers: {},
|
|
406
|
+
passed: false,
|
|
407
|
+
};
|
|
408
|
+
} finally {
|
|
409
|
+
clearTimeout(catalogTimeout);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let catalogPayload = null;
|
|
413
|
+
try {
|
|
414
|
+
catalogPayload = catalogText ? JSON.parse(catalogText) : null;
|
|
415
|
+
} catch {
|
|
416
|
+
// Report a typed catalog error below.
|
|
417
|
+
}
|
|
418
|
+
const models = Array.isArray(catalogPayload?.data) ? catalogPayload.data : [];
|
|
419
|
+
const catalogOrgId = typeof catalogPayload?.org_id === "string" ? catalogPayload.org_id : null;
|
|
420
|
+
const catalogProductAccess = typeof catalogPayload?.product_access === "string"
|
|
421
|
+
? catalogPayload.product_access
|
|
422
|
+
: null;
|
|
423
|
+
const catalogScopeMatches = catalogOrgId === tenantId && catalogProductAccess === productAccess;
|
|
424
|
+
const providerStatusResult = providerCatalogStatuses(catalogPayload);
|
|
425
|
+
const providerStatus = providerStatusResult.statuses;
|
|
426
|
+
const catalog = {
|
|
427
|
+
status: response.status,
|
|
428
|
+
durationMs: round(now() - catalogStartedAt),
|
|
429
|
+
models: models.length,
|
|
430
|
+
orgId: catalogOrgId,
|
|
431
|
+
productAccess: catalogProductAccess,
|
|
432
|
+
providerStatus,
|
|
433
|
+
error: response.ok && catalogPayload
|
|
434
|
+
? null
|
|
435
|
+
: response.ok
|
|
436
|
+
? "gateway returned an invalid model catalog"
|
|
437
|
+
: safeErrorMessage(catalogPayload, `${response.status} ${response.statusText || "gateway error"}`.trim()),
|
|
438
|
+
};
|
|
439
|
+
if (!response.ok || !catalogPayload) {
|
|
440
|
+
return { tenantId, productAccess, catalog, records: [], providers: {}, passed: false };
|
|
441
|
+
}
|
|
442
|
+
if (!catalogScopeMatches) {
|
|
443
|
+
catalog.error = "model catalog tenant or product access did not match the live selection";
|
|
444
|
+
return { tenantId, productAccess, catalog, records: [], providers: {}, passed: false };
|
|
445
|
+
}
|
|
446
|
+
if (providerStatusResult.malformed) {
|
|
447
|
+
catalog.error = "model catalog returned malformed provider status metadata";
|
|
448
|
+
return { tenantId, productAccess, catalog, records: [], providers: {}, passed: false };
|
|
449
|
+
}
|
|
450
|
+
const contradictoryProvider = providers.find((provider) => {
|
|
451
|
+
const status = providerStatus[provider];
|
|
452
|
+
return Boolean(selectModel(models, provider))
|
|
453
|
+
&& Boolean(status)
|
|
454
|
+
&& (status.state !== "healthy" || status.routable !== true);
|
|
455
|
+
});
|
|
456
|
+
if (contradictoryProvider) {
|
|
457
|
+
catalog.error = `model catalog advertised a ready ${contradictoryProvider} model while its provider status was not healthy and routable`;
|
|
458
|
+
return { tenantId, productAccess, catalog, records: [], providers: {}, passed: false };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const records = [];
|
|
462
|
+
const skippedProviders = new Set();
|
|
463
|
+
for (const provider of providers) {
|
|
464
|
+
const model = selectModel(models, provider);
|
|
465
|
+
if (!model) {
|
|
466
|
+
const status = providerStatus[provider];
|
|
467
|
+
if (status?.state === "no_seat" && status.routable === false) {
|
|
468
|
+
skippedProviders.add(provider);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
472
|
+
records.push({
|
|
473
|
+
tenantId,
|
|
474
|
+
provider,
|
|
475
|
+
attempt,
|
|
476
|
+
model: null,
|
|
477
|
+
requestId: null,
|
|
478
|
+
clientRequestId: null,
|
|
479
|
+
echoedClientRequestId: null,
|
|
480
|
+
responseId: null,
|
|
481
|
+
accountId: null,
|
|
482
|
+
orgId: null,
|
|
483
|
+
status: null,
|
|
484
|
+
headersMs: null,
|
|
485
|
+
firstByteMs: null,
|
|
486
|
+
ttftMs: null,
|
|
487
|
+
totalMs: null,
|
|
488
|
+
exactAck: false,
|
|
489
|
+
outputChars: 0,
|
|
490
|
+
outputTruncated: false,
|
|
491
|
+
terminalEvent: null,
|
|
492
|
+
doneSentinel: false,
|
|
493
|
+
receivedBytes: 0,
|
|
494
|
+
contentType: "",
|
|
495
|
+
error: "no ready model advertised for this tenant and provider",
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const url = new URL(provider === "claude" ? "/anthropic/v1/messages" : "/v1/responses", gatewayUrl);
|
|
501
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
502
|
+
records.push(await streamProbe({
|
|
503
|
+
provider,
|
|
504
|
+
url,
|
|
505
|
+
bearer,
|
|
506
|
+
model,
|
|
507
|
+
tenantId,
|
|
508
|
+
attempt,
|
|
509
|
+
timeoutMs,
|
|
510
|
+
fetchImpl,
|
|
511
|
+
uuid,
|
|
512
|
+
now,
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const providerSummary = Object.fromEntries(
|
|
518
|
+
providers.map((provider) => [
|
|
519
|
+
provider,
|
|
520
|
+
summarizeProvider(
|
|
521
|
+
records,
|
|
522
|
+
provider,
|
|
523
|
+
ttftBudgets[provider],
|
|
524
|
+
providerStatus[provider],
|
|
525
|
+
skippedProviders.has(provider),
|
|
526
|
+
),
|
|
527
|
+
]),
|
|
528
|
+
);
|
|
529
|
+
const probedProviders = providers.filter((provider) => !skippedProviders.has(provider));
|
|
530
|
+
const requestChecksPassed = probedProviders.length > 0
|
|
531
|
+
&& records.length === probedProviders.length * attempts
|
|
532
|
+
&& records.every(recordSucceeded);
|
|
533
|
+
const latencyPassed = probedProviders.length > 0 && probedProviders.every((provider) => {
|
|
534
|
+
const summary = providerSummary[provider];
|
|
535
|
+
return summary.attempted > 0 && summary.withinLatencyBudget === summary.attempted;
|
|
536
|
+
});
|
|
537
|
+
return {
|
|
538
|
+
tenantId,
|
|
539
|
+
productAccess,
|
|
540
|
+
catalog,
|
|
541
|
+
records,
|
|
542
|
+
providers: providerSummary,
|
|
543
|
+
latencyPassed,
|
|
544
|
+
passed: Boolean(productAccess) && requestChecksPassed && (!strictLatency || latencyPassed),
|
|
545
|
+
};
|
|
546
|
+
}
|