|
|
|
@@ -44,6 +44,19 @@ interface ServerLlm {
|
|
|
|
apiKeyRef?: { name: string; key: string } | null;
|
|
|
|
apiKeyRef?: { name: string; key: string } | null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Result of a live "say hi" probe against a server LLM. `ok` says we got a
|
|
|
|
|
|
|
|
* 200 + non-empty content back; `say` is the trimmed first 16 chars of the
|
|
|
|
|
|
|
|
* reply for the user to spot-check (most LLMs say "hi", a misbehaving one
|
|
|
|
|
|
|
|
* says "Hello! How can I assist…"). `ms` is end-to-end including TLS.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
export interface ServerLlmHealth {
|
|
|
|
|
|
|
|
ok: boolean;
|
|
|
|
|
|
|
|
ms: number;
|
|
|
|
|
|
|
|
say?: string;
|
|
|
|
|
|
|
|
error?: string;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface StatusCommandDeps {
|
|
|
|
export interface StatusCommandDeps {
|
|
|
|
configDeps: Partial<ConfigLoaderDeps>;
|
|
|
|
configDeps: Partial<ConfigLoaderDeps>;
|
|
|
|
credentialsDeps: Partial<CredentialsDeps>;
|
|
|
|
credentialsDeps: Partial<CredentialsDeps>;
|
|
|
|
@@ -62,6 +75,12 @@ export interface StatusCommandDeps {
|
|
|
|
* command stays printable even when mcpd is unreachable.
|
|
|
|
* command stays printable even when mcpd is unreachable.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
fetchServerLlms: (mcpdUrl: string, token: string | null) => Promise<ServerLlm[] | null>;
|
|
|
|
fetchServerLlms: (mcpdUrl: string, token: string | null) => Promise<ServerLlm[] | null>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Probe a single server LLM with a tiny "say hi" prompt to check it's
|
|
|
|
|
|
|
|
* actually serving inference. Used per-LLM in parallel after the fetch.
|
|
|
|
|
|
|
|
* Always resolves (never throws) so one bad LLM doesn't sink the section.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
probeServerLlm: (mcpdUrl: string, name: string, token: string | null) => Promise<ServerLlmHealth>;
|
|
|
|
isTTY: boolean;
|
|
|
|
isTTY: boolean;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -205,6 +224,104 @@ function defaultFetchServerLlms(mcpdUrl: string, token: string | null): Promise<
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* POST a tiny "say hi" prompt to /api/v1/llms/<name>/infer and decide if
|
|
|
|
|
|
|
|
* the LLM actually serves inference. Returns ok=true when the response is
|
|
|
|
|
|
|
|
* 200 with non-empty content OR reasoning_content (thinking models often
|
|
|
|
|
|
|
|
* spend their token budget on the reasoning trace and never emit a
|
|
|
|
|
|
|
|
* `content` block, but they're clearly alive if reasoning came back).
|
|
|
|
|
|
|
|
* Otherwise ok=false with an error string suitable for one-line display.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* `max_tokens: 64` gives reasoning models enough headroom to emit
|
|
|
|
|
|
|
|
* something visible while still capping latency at ~1-2 sec on cheap
|
|
|
|
|
|
|
|
* models. The exact wording — "Reply with just: hi" — is more terse and
|
|
|
|
|
|
|
|
* closer to what a thinking model can short-circuit on without burning
|
|
|
|
|
|
|
|
* its entire budget on reasoning.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const PROBE_TIMEOUT_MS = 15_000;
|
|
|
|
|
|
|
|
const PROBE_BODY = JSON.stringify({
|
|
|
|
|
|
|
|
messages: [{ role: 'user', content: 'Reply with just: hi' }],
|
|
|
|
|
|
|
|
max_tokens: 64,
|
|
|
|
|
|
|
|
temperature: 0,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function defaultProbeServerLlm(mcpdUrl: string, name: string, token: string | null): Promise<ServerLlmHealth> {
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
|
|
const started = Date.now();
|
|
|
|
|
|
|
|
const u = new URL(`${mcpdUrl}/api/v1/llms/${encodeURIComponent(name)}/infer`);
|
|
|
|
|
|
|
|
const driver = u.protocol === 'https:' ? https : http;
|
|
|
|
|
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
|
|
|
'Content-Length': String(Buffer.byteLength(PROBE_BODY)),
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
if (token !== null) headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let req: http.ClientRequest;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
req = driver.request({
|
|
|
|
|
|
|
|
hostname: u.hostname,
|
|
|
|
|
|
|
|
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
|
|
|
|
|
|
path: u.pathname + u.search,
|
|
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
|
|
headers,
|
|
|
|
|
|
|
|
timeout: PROBE_TIMEOUT_MS,
|
|
|
|
|
|
|
|
}, (res) => {
|
|
|
|
|
|
|
|
const chunks: Buffer[] = [];
|
|
|
|
|
|
|
|
res.on('data', (c: Buffer) => chunks.push(c));
|
|
|
|
|
|
|
|
res.on('end', () => {
|
|
|
|
|
|
|
|
const ms = Date.now() - started;
|
|
|
|
|
|
|
|
const body = Buffer.concat(chunks).toString('utf-8');
|
|
|
|
|
|
|
|
if ((res.statusCode ?? 0) !== 200) {
|
|
|
|
|
|
|
|
// Pull out just the error message if the body is JSON, else the
|
|
|
|
|
|
|
|
// raw status — keeps the line tidy.
|
|
|
|
|
|
|
|
let msg = `HTTP ${String(res.statusCode ?? 0)}`;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const parsed = JSON.parse(body) as { error?: string };
|
|
|
|
|
|
|
|
if (typeof parsed.error === 'string') msg = parsed.error;
|
|
|
|
|
|
|
|
} catch { /* not JSON, fall through */ }
|
|
|
|
|
|
|
|
resolve({ ok: false, ms, error: msg.slice(0, 80) });
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
let content = '';
|
|
|
|
|
|
|
|
let reasoning = '';
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const parsed = JSON.parse(body) as {
|
|
|
|
|
|
|
|
choices?: Array<{ message?: { content?: string; reasoning_content?: string } }>;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const msg = parsed.choices?.[0]?.message;
|
|
|
|
|
|
|
|
content = msg?.content?.trim() ?? '';
|
|
|
|
|
|
|
|
reasoning = msg?.reasoning_content?.trim() ?? '';
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
resolve({ ok: false, ms, error: 'invalid response body' });
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (content !== '') {
|
|
|
|
|
|
|
|
resolve({ ok: true, ms, say: content.slice(0, 16) });
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reasoning !== '') {
|
|
|
|
|
|
|
|
// Thinking model burned its budget on the reasoning trace
|
|
|
|
|
|
|
|
// before emitting `content`. The LLM is alive — flag it as
|
|
|
|
|
|
|
|
// ok and surface a short reasoning preview so the user can
|
|
|
|
|
|
|
|
// tell at a glance.
|
|
|
|
|
|
|
|
resolve({ ok: true, ms, say: `[thinking] ${reasoning.slice(0, 12)}` });
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
resolve({ ok: false, ms, error: 'empty content' });
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
resolve({ ok: false, ms: Date.now() - started, error: 'request failed' });
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
req.on('error', (e) => resolve({ ok: false, ms: Date.now() - started, error: e.message.slice(0, 80) }));
|
|
|
|
|
|
|
|
req.on('timeout', () => { req.destroy(); resolve({ ok: false, ms: Date.now() - started, error: 'timeout' }); });
|
|
|
|
|
|
|
|
req.write(PROBE_BODY);
|
|
|
|
|
|
|
|
req.end();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
|
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
|
|
|
|
|
|
|
|
|
|
const defaultDeps: StatusCommandDeps = {
|
|
|
|
const defaultDeps: StatusCommandDeps = {
|
|
|
|
@@ -217,6 +334,7 @@ const defaultDeps: StatusCommandDeps = {
|
|
|
|
fetchModels: defaultFetchModels,
|
|
|
|
fetchModels: defaultFetchModels,
|
|
|
|
fetchProviders: defaultFetchProviders,
|
|
|
|
fetchProviders: defaultFetchProviders,
|
|
|
|
fetchServerLlms: defaultFetchServerLlms,
|
|
|
|
fetchServerLlms: defaultFetchServerLlms,
|
|
|
|
|
|
|
|
probeServerLlm: defaultProbeServerLlm,
|
|
|
|
isTTY: process.stdout.isTTY ?? false,
|
|
|
|
isTTY: process.stdout.isTTY ?? false,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@@ -278,7 +396,7 @@ function formatProviderStatus(name: string, info: ProvidersInfo, ansi: boolean):
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function createStatusCommand(deps?: Partial<StatusCommandDeps>): Command {
|
|
|
|
export function createStatusCommand(deps?: Partial<StatusCommandDeps>): Command {
|
|
|
|
const { configDeps, credentialsDeps, log, write, checkHealth, checkLlm, fetchModels, fetchProviders, fetchServerLlms, isTTY } = { ...defaultDeps, ...deps };
|
|
|
|
const { configDeps, credentialsDeps, log, write, checkHealth, checkLlm, fetchModels, fetchProviders, fetchServerLlms, probeServerLlm, isTTY } = { ...defaultDeps, ...deps };
|
|
|
|
|
|
|
|
|
|
|
|
return new Command('status')
|
|
|
|
return new Command('status')
|
|
|
|
.description('Show mcpctl status and connectivity')
|
|
|
|
.description('Show mcpctl status and connectivity')
|
|
|
|
@@ -292,14 +410,25 @@ export function createStatusCommand(deps?: Partial<StatusCommandDeps>): Command
|
|
|
|
|
|
|
|
|
|
|
|
if (opts.output !== 'table') {
|
|
|
|
if (opts.output !== 'table') {
|
|
|
|
// JSON/YAML: run everything in parallel, wait, output at once
|
|
|
|
// JSON/YAML: run everything in parallel, wait, output at once
|
|
|
|
|
|
|
|
const token = creds?.token ?? null;
|
|
|
|
const [mcplocalReachable, mcpdReachable, llmStatus, providersInfo, serverLlms] = await Promise.all([
|
|
|
|
const [mcplocalReachable, mcpdReachable, llmStatus, providersInfo, serverLlms] = await Promise.all([
|
|
|
|
checkHealth(config.mcplocalUrl),
|
|
|
|
checkHealth(config.mcplocalUrl),
|
|
|
|
checkHealth(config.mcpdUrl),
|
|
|
|
checkHealth(config.mcpdUrl),
|
|
|
|
llmLabel ? checkLlm(config.mcplocalUrl) : Promise.resolve(null),
|
|
|
|
llmLabel ? checkLlm(config.mcplocalUrl) : Promise.resolve(null),
|
|
|
|
multiProvider ? fetchProviders(config.mcplocalUrl) : Promise.resolve(null),
|
|
|
|
multiProvider ? fetchProviders(config.mcplocalUrl) : Promise.resolve(null),
|
|
|
|
fetchServerLlms(config.mcpdUrl, creds?.token ?? null),
|
|
|
|
fetchServerLlms(config.mcpdUrl, token),
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Probe each server LLM in parallel — adds 0-2 sec to JSON mode but
|
|
|
|
|
|
|
|
// gives consumers (scripts, dashboards) the same liveness signal as
|
|
|
|
|
|
|
|
// the table view.
|
|
|
|
|
|
|
|
const serverLlmsWithHealth = serverLlms !== null
|
|
|
|
|
|
|
|
? await Promise.all(serverLlms.map(async (l) => ({
|
|
|
|
|
|
|
|
...l,
|
|
|
|
|
|
|
|
health: await probeServerLlm(config.mcpdUrl, l.name, token),
|
|
|
|
|
|
|
|
})))
|
|
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
const llm = llmLabel
|
|
|
|
const llm = llmLabel
|
|
|
|
? llmStatus === 'ok' ? llmLabel : `${llmLabel} (${llmStatus})`
|
|
|
|
? llmStatus === 'ok' ? llmLabel : `${llmLabel} (${llmStatus})`
|
|
|
|
: null;
|
|
|
|
: null;
|
|
|
|
@@ -316,7 +445,7 @@ export function createStatusCommand(deps?: Partial<StatusCommandDeps>): Command
|
|
|
|
llm,
|
|
|
|
llm,
|
|
|
|
llmStatus,
|
|
|
|
llmStatus,
|
|
|
|
...(providersInfo ? { providers: providersInfo } : {}),
|
|
|
|
...(providersInfo ? { providers: providersInfo } : {}),
|
|
|
|
...(serverLlms !== null ? { serverLlms } : {}),
|
|
|
|
...(serverLlmsWithHealth !== null ? { serverLlms: serverLlmsWithHealth } : {}),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
log(opts.output === 'json' ? formatJson(status) : formatYaml(status));
|
|
|
|
log(opts.output === 'json' ? formatJson(status) : formatYaml(status));
|
|
|
|
@@ -341,11 +470,12 @@ export function createStatusCommand(deps?: Partial<StatusCommandDeps>): Command
|
|
|
|
// Server LLMs (mcpd-managed) — fetched in parallel regardless of the
|
|
|
|
// Server LLMs (mcpd-managed) — fetched in parallel regardless of the
|
|
|
|
// local-LLM config, so the section renders even on machines without
|
|
|
|
// local-LLM config, so the section renders even on machines without
|
|
|
|
// a configured client-side provider.
|
|
|
|
// a configured client-side provider.
|
|
|
|
const serverLlmsPromise = fetchServerLlms(config.mcpdUrl, creds?.token ?? null);
|
|
|
|
const token = creds?.token ?? null;
|
|
|
|
|
|
|
|
const serverLlmsPromise = fetchServerLlms(config.mcpdUrl, token);
|
|
|
|
|
|
|
|
|
|
|
|
if (!llmLabel) {
|
|
|
|
if (!llmLabel) {
|
|
|
|
log(`LLM: not configured (run 'mcpctl config setup')`);
|
|
|
|
log(`LLM: not configured (run 'mcpctl config setup')`);
|
|
|
|
await renderServerLlmsSection(serverLlmsPromise, isTTY);
|
|
|
|
await renderServerLlmsSection(serverLlmsPromise, config.mcpdUrl, token, isTTY);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -409,17 +539,20 @@ export function createStatusCommand(deps?: Partial<StatusCommandDeps>): Command
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await renderServerLlmsSection(serverLlmsPromise, isTTY);
|
|
|
|
await renderServerLlmsSection(serverLlmsPromise, config.mcpdUrl, token, isTTY);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Print a "Server LLMs:" section listing mcpd-managed Llm rows by tier.
|
|
|
|
* Print a "Server LLMs:" section listing mcpd-managed Llm rows by tier
|
|
|
|
* These are the rows created via `mcpctl create llm` — distinct from the
|
|
|
|
* with a per-LLM "say hi" liveness probe. Distinct from the mcplocal-side
|
|
|
|
* mcplocal-side providers shown by the existing "LLM:" lines above. The
|
|
|
|
* providers shown by the existing "LLM:" lines above. The caller awaits a
|
|
|
|
* caller awaits a pre-launched promise so this doesn't add round-trips.
|
|
|
|
* pre-launched promise so this doesn't add fetch round-trips, but the
|
|
|
|
|
|
|
|
* probe itself runs here (after the user has the rest of the screen).
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
async function renderServerLlmsSection(
|
|
|
|
async function renderServerLlmsSection(
|
|
|
|
serverLlmsPromise: Promise<ServerLlm[] | null>,
|
|
|
|
serverLlmsPromise: Promise<ServerLlm[] | null>,
|
|
|
|
|
|
|
|
mcpdUrl: string,
|
|
|
|
|
|
|
|
token: string | null,
|
|
|
|
ansi: boolean,
|
|
|
|
ansi: boolean,
|
|
|
|
): Promise<void> {
|
|
|
|
): Promise<void> {
|
|
|
|
const llms = await serverLlmsPromise;
|
|
|
|
const llms = await serverLlmsPromise;
|
|
|
|
@@ -433,6 +566,14 @@ export function createStatusCommand(deps?: Partial<StatusCommandDeps>): Command
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log(`Server LLMs: ${String(llms.length)} registered ${ansi ? DIM : ''}(probing live "say hi"...)${ansi ? RESET : ''}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Run all probes in parallel — one slow LLM doesn't block the others.
|
|
|
|
|
|
|
|
const healthByName = new Map<string, ServerLlmHealth>();
|
|
|
|
|
|
|
|
await Promise.all(llms.map(async (l) => {
|
|
|
|
|
|
|
|
healthByName.set(l.name, await probeServerLlm(mcpdUrl, l.name, token));
|
|
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const byTier = new Map<string, ServerLlm[]>();
|
|
|
|
const byTier = new Map<string, ServerLlm[]>();
|
|
|
|
for (const l of llms) {
|
|
|
|
for (const l of llms) {
|
|
|
|
const arr = byTier.get(l.tier) ?? [];
|
|
|
|
const arr = byTier.get(l.tier) ?? [];
|
|
|
|
@@ -440,19 +581,42 @@ export function createStatusCommand(deps?: Partial<StatusCommandDeps>): Command
|
|
|
|
byTier.set(l.tier, arr);
|
|
|
|
byTier.set(l.tier, arr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log(`Server LLMs: ${String(llms.length)} registered`);
|
|
|
|
|
|
|
|
// Print tiers in a stable order — fast/heavy first, then anything else.
|
|
|
|
// Print tiers in a stable order — fast/heavy first, then anything else.
|
|
|
|
const tierOrder = ['fast', 'heavy', ...[...byTier.keys()].filter((t) => t !== 'fast' && t !== 'heavy').sort()];
|
|
|
|
const tierOrder = ['fast', 'heavy', ...[...byTier.keys()].filter((t) => t !== 'fast' && t !== 'heavy').sort()];
|
|
|
|
for (const tier of tierOrder) {
|
|
|
|
for (const tier of tierOrder) {
|
|
|
|
const rows = byTier.get(tier);
|
|
|
|
const rows = byTier.get(tier);
|
|
|
|
if (rows === undefined || rows.length === 0) continue;
|
|
|
|
if (rows === undefined || rows.length === 0) continue;
|
|
|
|
const formatted = rows.map((r) => {
|
|
|
|
const formatted = rows.map((r) => formatServerLlmLine(r, healthByName.get(r.name), ansi));
|
|
|
|
const upstream = r.url !== '' ? r.url : 'provider default';
|
|
|
|
|
|
|
|
const auth = r.apiKeyRef ? `key:${r.apiKeyRef.name}/${r.apiKeyRef.key}` : 'no key';
|
|
|
|
|
|
|
|
const line = `${r.name} (${r.type} → ${r.model}) ${upstream} ${auth}`;
|
|
|
|
|
|
|
|
return ansi ? `${DIM}${line}${RESET}` : line;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
log(` ${tier.padEnd(6)} ${formatted.join('\n ')}`);
|
|
|
|
log(` ${tier.padEnd(6)} ${formatted.join('\n ')}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Format a single server-LLM row plus its health-probe outcome on one line.
|
|
|
|
|
|
|
|
* Exported via module scope (not closure) so it stays cheap to test in
|
|
|
|
|
|
|
|
* isolation; takes \`ansi\` rather than reading a TTY at call time.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
function formatServerLlmLine(r: ServerLlm, h: ServerLlmHealth | undefined, ansi: boolean): string {
|
|
|
|
|
|
|
|
const upstream = r.url !== '' ? r.url : 'provider default';
|
|
|
|
|
|
|
|
const auth = r.apiKeyRef ? `key:${r.apiKeyRef.name}/${r.apiKeyRef.key}` : 'no key';
|
|
|
|
|
|
|
|
let healthStr: string;
|
|
|
|
|
|
|
|
if (h === undefined) {
|
|
|
|
|
|
|
|
healthStr = ansi ? `${DIM}? probe skipped${RESET}` : '? probe skipped';
|
|
|
|
|
|
|
|
} else if (h.ok) {
|
|
|
|
|
|
|
|
const reply = h.say !== undefined ? `"${h.say}"` : 'ok';
|
|
|
|
|
|
|
|
const ms = `${String(h.ms)}ms`;
|
|
|
|
|
|
|
|
healthStr = ansi ? `${GREEN}✓ ${reply} ${DIM}${ms}${RESET}` : `✓ ${reply} ${ms}`;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
const err = h.error ?? 'failed';
|
|
|
|
|
|
|
|
healthStr = ansi ? `${RED}✗ ${err}${RESET}` : `✗ ${err}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const meta = `${r.type} → ${r.model}`;
|
|
|
|
|
|
|
|
// Two-line layout: name + health on top, dim metadata indented below.
|
|
|
|
|
|
|
|
// Keeps the at-a-glance signal (✓/✗) close to the LLM name.
|
|
|
|
|
|
|
|
const head = `${r.name} ${healthStr}`;
|
|
|
|
|
|
|
|
const tail = ansi
|
|
|
|
|
|
|
|
? `${DIM}${meta} ${upstream} ${auth}${RESET}`
|
|
|
|
|
|
|
|
: `${meta} ${upstream} ${auth}`;
|
|
|
|
|
|
|
|
return `${head}\n ${tail}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|