feat(web): provider management UI — list, test, model capabilities (#123)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
- Add POST /api/providers/test endpoint for connection testing with latency and model discovery - Add provider.dto.ts with TestConnectionDto and TestConnectionResultDto - Rewrite settings page with full provider management UI: status badges, expandable model tables, capability badges (chat/reasoning/vision), default model indicator, and test connection button with result banner Fixes #123 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||
import { ModelRegistry, AuthStorage } from '@mariozechner/pi-coding-agent';
|
||||
import type { Model, Api } from '@mariozechner/pi-ai';
|
||||
import type { ModelInfo, ProviderInfo, CustomProviderConfig } from '@mosaic/types';
|
||||
import type { TestConnectionResultDto } from './provider.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class ProviderService implements OnModuleInit {
|
||||
@@ -64,6 +65,63 @@ export class ProviderService implements OnModuleInit {
|
||||
return this.registry.getAvailable().map((m) => this.toModelInfo(m));
|
||||
}
|
||||
|
||||
async testConnection(providerId: string, baseUrl?: string): Promise<TestConnectionResultDto> {
|
||||
// Resolve baseUrl: explicit override > registered provider > ollama env
|
||||
let resolvedUrl = baseUrl;
|
||||
|
||||
if (!resolvedUrl) {
|
||||
const allModels = this.registry.getAll();
|
||||
const providerModels = allModels.filter((m) => m.provider === providerId);
|
||||
if (providerModels.length === 0) {
|
||||
return { providerId, reachable: false, error: `Provider '${providerId}' not found` };
|
||||
}
|
||||
// For Ollama, derive the base URL from environment
|
||||
if (providerId === 'ollama') {
|
||||
const ollamaUrl = process.env['OLLAMA_BASE_URL'] ?? process.env['OLLAMA_HOST'];
|
||||
if (!ollamaUrl) {
|
||||
return { providerId, reachable: false, error: 'OLLAMA_BASE_URL not configured' };
|
||||
}
|
||||
resolvedUrl = `${ollamaUrl}/v1/models`;
|
||||
} else {
|
||||
// For other providers, we can only do a basic check
|
||||
return { providerId, reachable: true, discoveredModels: providerModels.map((m) => m.id) };
|
||||
}
|
||||
} else {
|
||||
resolvedUrl = resolvedUrl.replace(/\/?$/, '') + '/models';
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
try {
|
||||
const res = await fetch(resolvedUrl, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
const latencyMs = Date.now() - start;
|
||||
|
||||
if (!res.ok) {
|
||||
return { providerId, reachable: false, latencyMs, error: `HTTP ${res.status}` };
|
||||
}
|
||||
|
||||
let discoveredModels: string[] | undefined;
|
||||
try {
|
||||
const json = (await res.json()) as { models?: Array<{ id?: string; name?: string }> };
|
||||
if (Array.isArray(json.models)) {
|
||||
discoveredModels = json.models.map((m) => m.id ?? m.name ?? '').filter(Boolean);
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors — endpoint was reachable
|
||||
}
|
||||
|
||||
return { providerId, reachable: true, latencyMs, discoveredModels };
|
||||
} catch (err) {
|
||||
const latencyMs = Date.now() - start;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { providerId, reachable: false, latencyMs, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
registerCustomProvider(config: CustomProviderConfig): void {
|
||||
this.registry.registerProvider(config.id, {
|
||||
baseUrl: config.baseUrl,
|
||||
|
||||
Reference in New Issue
Block a user