feat(profiles): add local runtime readiness checks

This commit is contained in:
Tam Nhu Tran
2026-04-10 05:49:38 -04:00
parent 29ef0c8e04
commit 63b67f1c1c
6 changed files with 307 additions and 0 deletions
+6
View File
@@ -64,6 +64,12 @@ export {
// OpenRouter catalog and picker
export { isOpenRouterUrl, fetchOpenRouterModels, type OpenRouterModel } from './openrouter-catalog';
export { pickOpenRouterModel, type OpenRouterSelection } from './openrouter-picker';
export {
getLocalRuntimeReadiness,
type LocalRuntimeId,
type LocalRuntimeReadiness,
type LocalRuntimeStatus,
} from './local-runtime-readiness';
// Provider presets for CLI
export {
+121
View File
@@ -0,0 +1,121 @@
export type LocalRuntimeId = 'ollama' | 'llamacpp';
export type LocalRuntimeStatus = 'ready' | 'missing-model' | 'offline';
export interface LocalRuntimeReadiness {
id: LocalRuntimeId;
name: string;
endpoint: string;
status: LocalRuntimeStatus;
commandHint: string;
recommendedModel: string | null;
recommendedModelInstalled: boolean;
detectedModelCount: number;
}
interface LocalRuntimeDefinition {
id: LocalRuntimeId;
name: string;
endpoint: string;
modelsUrl: string;
commandHint: string;
recommendedModel: string | null;
parseModelIds: (payload: unknown) => string[];
}
const LOCAL_RUNTIME_DEFINITIONS: LocalRuntimeDefinition[] = [
{
id: 'ollama',
name: 'Ollama',
endpoint: 'http://127.0.0.1:11434',
modelsUrl: 'http://127.0.0.1:11434/api/tags',
commandHint: 'ollama serve',
recommendedModel: 'gemma4:e4b',
parseModelIds: (payload) => {
const models = (payload as { models?: Array<{ name?: string }> })?.models;
return Array.isArray(models)
? models
.map((model) => (typeof model?.name === 'string' ? model.name.trim() : ''))
.filter(Boolean)
: [];
},
},
{
id: 'llamacpp',
name: 'llama.cpp',
endpoint: 'http://127.0.0.1:8080',
modelsUrl: 'http://127.0.0.1:8080/v1/models',
commandHint: './server --host 0.0.0.0 --port 8080 -m model.gguf',
recommendedModel: null,
parseModelIds: (payload) => {
const models = (payload as { data?: Array<{ id?: string }> })?.data;
return Array.isArray(models)
? models
.map((model) => (typeof model?.id === 'string' ? model.id.trim() : ''))
.filter(Boolean)
: [];
},
},
];
async function fetchModelIds(definition: LocalRuntimeDefinition): Promise<string[]> {
const response = await fetch(definition.modelsUrl, {
signal: AbortSignal.timeout(1500),
});
if (!response.ok) {
throw new Error(`${definition.id} readiness probe failed (${response.status})`);
}
return definition.parseModelIds(await response.json());
}
function toReadiness(
definition: LocalRuntimeDefinition,
modelIds: string[]
): LocalRuntimeReadiness {
const recommendedModelInstalled = definition.recommendedModel
? modelIds.some((modelId) => modelId === definition.recommendedModel)
: modelIds.length > 0;
const status: LocalRuntimeStatus =
modelIds.length === 0 || !recommendedModelInstalled ? 'missing-model' : 'ready';
return {
id: definition.id,
name: definition.name,
endpoint: definition.endpoint,
status,
commandHint:
status === 'missing-model' && definition.recommendedModel
? `ollama pull ${definition.recommendedModel}`
: definition.commandHint,
recommendedModel: definition.recommendedModel,
recommendedModelInstalled,
detectedModelCount: modelIds.length,
};
}
function toOffline(definition: LocalRuntimeDefinition): LocalRuntimeReadiness {
return {
id: definition.id,
name: definition.name,
endpoint: definition.endpoint,
status: 'offline',
commandHint: definition.commandHint,
recommendedModel: definition.recommendedModel,
recommendedModelInstalled: false,
detectedModelCount: 0,
};
}
export async function getLocalRuntimeReadiness(): Promise<LocalRuntimeReadiness[]> {
return Promise.all(
LOCAL_RUNTIME_DEFINITIONS.map(async (definition) => {
try {
return toReadiness(definition, await fetchModelIds(definition));
} catch {
return toOffline(definition);
}
})
);
}
+9
View File
@@ -10,6 +10,7 @@ import { isReservedName, RESERVED_PROFILE_NAMES } from '../../config/reserved-na
import {
createApiProfile,
createCliproxyBridgeProfile,
getLocalRuntimeReadiness,
removeApiProfile,
updateApiProfileTarget,
discoverApiProfileOrphans,
@@ -86,6 +87,14 @@ router.get('/cliproxy-bridge/providers', (_req: Request, res: Response): void =>
}
});
router.get('/local-runtime-readiness', async (_req: Request, res: Response): Promise<void> => {
try {
res.json({ runtimes: await getLocalRuntimeReadiness() });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
router.post('/cliproxy-bridge', (req: Request, res: Response): void => {
const shape = validatePayloadShape(req.body, ['provider', 'name', 'target']);
if (!shape.ok) {
@@ -0,0 +1,141 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
import express from 'express';
import type { Server } from 'http';
import profileRoutes from '../../../src/web-server/routes/profile-routes';
describe('profile-routes local runtime readiness', () => {
let server: Server;
let baseUrl = '';
const originalFetch = globalThis.fetch;
beforeAll(async () => {
const app = express();
app.use(express.json());
app.use('/api/profiles', profileRoutes);
await new Promise<void>((resolve, reject) => {
server = app.listen(0, '127.0.0.1');
const onError = (error: Error) => reject(error);
server.once('error', onError);
server.once('listening', () => {
server.off('error', onError);
resolve();
});
});
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Unable to resolve test server port');
}
baseUrl = `http://127.0.0.1:${address.port}`;
});
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
beforeEach(() => {
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith(baseUrl)) {
return originalFetch(input);
}
if (url.includes('11434/api/tags')) {
return new Response(
JSON.stringify({
models: [{ name: 'gemma4:e4b' }, { name: 'qwen3-coder:latest' }],
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
}
if (url.includes('8080/v1/models')) {
return new Response(
JSON.stringify({
data: [{ id: 'Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf' }],
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
}
throw new Error(`Unexpected URL: ${url}`);
}) as typeof fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('reports local runtimes as ready when their endpoints respond with models', async () => {
const response = await fetch(`${baseUrl}/api/profiles/local-runtime-readiness`);
expect(response.status).toBe(200);
const body = (await response.json()) as {
runtimes: Array<{
id: string;
status: string;
recommendedModelInstalled: boolean;
}>;
};
expect(body.runtimes).toHaveLength(2);
expect(body.runtimes).toContainEqual(
expect.objectContaining({
id: 'ollama',
status: 'ready',
recommendedModelInstalled: true,
})
);
expect(body.runtimes).toContainEqual(
expect.objectContaining({
id: 'llamacpp',
status: 'ready',
})
);
});
it('reports setup guidance when local endpoints are unavailable', async () => {
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith(baseUrl)) {
return originalFetch(input);
}
throw new Error('connect ECONNREFUSED');
}) as typeof fetch;
const response = await fetch(`${baseUrl}/api/profiles/local-runtime-readiness`);
expect(response.status).toBe(200);
const body = (await response.json()) as {
runtimes: Array<{
id: string;
status: string;
commandHint: string;
}>;
};
expect(body.runtimes).toContainEqual(
expect.objectContaining({
id: 'ollama',
status: 'offline',
commandHint: 'ollama serve',
})
);
expect(body.runtimes).toContainEqual(
expect.objectContaining({
id: 'llamacpp',
status: 'offline',
commandHint: './server --host 0.0.0.0 --port 8080 -m model.gguf',
})
);
});
});
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api-client';
export function useLocalRuntimeReadiness() {
return useQuery({
queryKey: ['profiles', 'local-runtime-readiness'],
queryFn: () => api.profiles.getLocalRuntimeReadiness(),
staleTime: 15_000,
refetchOnWindowFocus: false,
});
}
+19
View File
@@ -239,6 +239,23 @@ export interface Profile {
cliproxyBridge?: CliproxyBridgeMetadata | null;
}
export type LocalRuntimeStatus = 'ready' | 'missing-model' | 'offline';
export interface LocalRuntimeReadiness {
id: 'ollama' | 'llamacpp';
name: string;
endpoint: string;
status: LocalRuntimeStatus;
commandHint: string;
recommendedModel: string | null;
recommendedModelInstalled: boolean;
detectedModelCount: number;
}
export interface LocalRuntimeReadinessResponse {
runtimes: LocalRuntimeReadiness[];
}
export interface CreateProfile {
name: string;
baseUrl: string;
@@ -1024,6 +1041,8 @@ export interface CliproxyRestartResult {
export const api = {
profiles: {
list: () => request<{ profiles: Profile[] }>('/profiles'),
getLocalRuntimeReadiness: () =>
request<LocalRuntimeReadinessResponse>('/profiles/local-runtime-readiness'),
create: (data: CreateProfile) =>
request('/profiles', {
method: 'POST',