diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx index 30a0a7f9..b48e29d4 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Plus, Save, Trash2, RefreshCw, Loader2, X } from 'lucide-react'; +import { Plus, Save, Trash2, RefreshCw, Loader2, X, Pencil } from 'lucide-react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; @@ -112,6 +112,13 @@ export const AIHarnessesSection = () => { const connectedProviders = PI_PROVIDERS.filter((p) => storedPiIds.has(p.piId)); const unconnectedProviders = PI_PROVIDERS.filter((p) => !storedPiIds.has(p.piId)); + const { data: remoteHealth = {} as Record } = useQuery({ + queryKey: ['PI_MONO_REMOTE_HEALTH'], + queryFn: () => client.get>('/server-settings/pi-mono/api-keys/health'), + enabled: connectedProviders.length > 0, + refetchInterval: 30_000, + }); + const getStoredMasked = (piId: string) => storedKeys.find((k) => k.provider === piId)?.value ?? ''; @@ -122,12 +129,14 @@ export const AIHarnessesSection = () => { try { await client.put('/server-settings/pi-mono/api-keys', { provider: piId, value }); queryClient.invalidateQueries({ queryKey: ['PI_MONO_API_KEYS'] }); + queryClient.invalidateQueries({ queryKey: ['PI_MONO_REMOTE_HEALTH'] }); queryClient.invalidateQueries({ queryKey: ['PI_MODELS'] }); setKeyInputs((prev) => { const next = { ...prev }; delete next[piId]; return next; }); + setEditingProvider(null); } finally { setSavingKey(null); } @@ -136,6 +145,7 @@ export const AIHarnessesSection = () => { const disconnectProvider = async (provider: typeof PI_PROVIDERS[number]) => { await client.put('/server-settings/pi-mono/api-keys', { provider: provider.piId, value: '' }); queryClient.invalidateQueries({ queryKey: ['PI_MONO_API_KEYS'] }); + queryClient.invalidateQueries({ queryKey: ['PI_MONO_REMOTE_HEALTH'] }); queryClient.invalidateQueries({ queryKey: ['PI_MODELS'] }); if (editingProvider === provider.key) setEditingProvider(null); }; @@ -422,37 +432,79 @@ export const AIHarnessesSection = () => { Remote Providers
{connectedProviders.map((provider) => ( -
- - setKeyInputs((prev) => ({ ...prev, [provider.piId]: ev.target.value }))} - /> - - +
+
+ + {provider.key} + + +
+ {editingProvider === provider.key && ( +
+ setKeyInputs((prev) => ({ ...prev, [provider.piId]: ev.target.value }))} + onKeyDown={(ev) => { + if (ev.key === 'Enter' && keyInputs[provider.piId]) saveApiKey(provider.piId); + if (ev.key === 'Escape') setEditingProvider(null); + }} + autoFocus + /> + + +
+ )}
))} {editingProvider && (() => { const provider = PI_PROVIDERS.find((p) => p.key === editingProvider); - if (!provider || connectedProviders.includes(provider)) return null; + if (!provider || connectedProviders.some((cp) => cp.piId === provider.piId)) return null; return (
@@ -462,6 +514,10 @@ export const AIHarnessesSection = () => { placeholder="Enter API key" value={keyInputs[provider.piId] ?? ''} onChange={(ev) => setKeyInputs((prev) => ({ ...prev, [provider.piId]: ev.target.value }))} + onKeyDown={(ev) => { + if (ev.key === 'Enter' && keyInputs[provider.piId]) saveApiKey(provider.piId); + if (ev.key === 'Escape') setEditingProvider(null); + }} autoFocus /> +
); })()} diff --git a/src/servers/api/server-settings/pi-mono.ts b/src/servers/api/server-settings/pi-mono.ts index 2641ede9..8ff1e4e2 100644 --- a/src/servers/api/server-settings/pi-mono.ts +++ b/src/servers/api/server-settings/pi-mono.ts @@ -387,6 +387,52 @@ piMonoRouter.put('/api-keys', async (ctx) => { return ctx.json({ ok: true }); }); +const REMOTE_HEALTH_CONFIG: Record string); + headers: (key: string) => Record; +}> = { + openai: { url: 'https://api.openai.com/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) }, + anthropic: { url: 'https://api.anthropic.com/v1/models', headers: (k) => ({ 'x-api-key': k, 'anthropic-version': '2023-06-01' }) }, + google: { url: (k) => `https://generativelanguage.googleapis.com/v1beta/models?key=${k}`, headers: () => ({}) }, + groq: { url: 'https://api.groq.com/openai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) }, + mistral: { url: 'https://api.mistral.ai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) }, + xai: { url: 'https://api.x.ai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) }, + openrouter: { url: 'https://openrouter.ai/api/v1/auth/key', headers: (k) => ({ Authorization: `Bearer ${k}` }) }, + cerebras: { url: 'https://api.cerebras.ai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) }, + zai: { url: 'https://opencode.ai/zen/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) }, +}; + +piMonoRouter.get('/api-keys/health', async (ctx) => { + const auth = await readAuthJson(); + const results: Record = {}; + + const checks = PROVIDERS + .filter((p) => auth[p.piId]?.key?.trim()) + .map(async (p) => { + const config = REMOTE_HEALTH_CONFIG[p.piId]; + if (!config) { + results[p.piId] = null; + return; + } + const key = auth[p.piId]!.key; + const url = typeof config.url === 'function' ? config.url(key) : config.url; + const headers = config.headers(key); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 3000); + try { + const res = await fetch(url, { headers, signal: controller.signal }); + results[p.piId] = res.ok; + } catch { + results[p.piId] = false; + } finally { + clearTimeout(timer); + } + }); + + await Promise.all(checks); + return ctx.json(results); +}); + const GLOBAL_DIRS = ['/usr/local/bin', '/usr/bin']; const getPaths = async () => {