This commit is contained in:
Mikan
2026-06-19 17:32:21 +03:00
parent 81bbf8aa69
commit 2167493887
6 changed files with 484 additions and 32 deletions

View File

@@ -82,6 +82,43 @@ export const adminApi = {
const { data } = await api.post("/admin/embeddings/test", { overrides: overrides || {} });
return data;
},
testLlm: async (overrides?: Record<string, any>): Promise<{
ok: boolean;
base_url: string;
model: string;
dns_resolved?: boolean;
resolved_addrs?: string[];
tcp_connect_ok?: boolean;
models_endpoint_status?: number;
available_models?: string[];
chat_endpoint_status?: number;
latency_ms?: number;
response_preview?: string;
usage?: any;
error?: string;
error_type?: string;
}> => {
const { data } = await api.post("/admin/llm/test", { overrides: overrides || {} });
return data;
},
testLlmTools: async (overrides?: Record<string, any>): Promise<{
ok: boolean;
base_url: string;
model: string;
tool_calls_returned: boolean;
tool_call_name?: string;
tool_call_args?: any;
text?: string;
raw_tool_calls?: any[];
http_status?: number;
latency_ms?: number;
usage?: any;
error?: string;
error_type?: string;
}> => {
const { data } = await api.post("/admin/llm/test-tools", { overrides: overrides || {} });
return data;
},
listLlmLogs: async (limit = 50, offset = 0): Promise<LlmLog[]> => {
const { data } = await api.get(`/admin/llm-logs?limit=${limit}&offset=${offset}`);
return data;

View File

@@ -119,6 +119,15 @@ export const en = {
embedding_testing: "Testing...",
embedding_test_ok: "OK: {{provider}} | dim={{dim}} | norm={{norm}}",
embedding_test_fail: "Error: {{error}}",
llm_test: "Test LLM",
llm_test_testing: "Testing...",
llm_test_tools: "Test LLM (with tools)",
llm_test_tools_testing: "Testing tool calls...",
llm_test_ok: "OK ({{latency}}ms): {{preview}}",
llm_test_fail: "Error: {{error}}",
llm_test_tools_ok: "Tool calls: {{ok}} | name={{name}} | args={{args}}",
llm_test_tools_ok_with_call: "OK — model called {{name}}({{args}}) in {{latency}}ms",
llm_test_tools_ok_no_call: "WARNING — model responded but did NOT call the tool. Text: {{text}}",
save: "Save",
saved: "Saved!",
llm_logs: "LLM logs",

View File

@@ -119,6 +119,15 @@ export const ru = {
embedding_testing: "Проверяю...",
embedding_test_ok: "OK: {{provider}} | dim={{dim}} | norm={{norm}}",
embedding_test_fail: "Ошибка: {{error}}",
llm_test: "Проверить LLM",
llm_test_testing: "Проверяю...",
llm_test_tools: "Проверить LLM (с инструментами)",
llm_test_tools_testing: "Проверяю вызовы инструментов...",
llm_test_ok: "OK ({{latency}}мс): {{preview}}",
llm_test_fail: "Ошибка: {{error}}",
llm_test_tools_ok: "Вызовы инструментов: {{ok}} | имя={{name}} | аргументы={{args}}",
llm_test_tools_ok_with_call: "OK — модель вызвала {{name}}({{args}}) за {{latency}}мс",
llm_test_tools_ok_no_call: "ВНИМАНИЕ — модель ответила, но НЕ вызвала инструмент. Текст: {{text}}",
save: "Сохранить",
saved: "Сохранено!",
llm_logs: "Логи LLM",

View File

@@ -6,7 +6,7 @@ import type { LlmLog, SettingsOut } from "@/types";
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { Card, CardBody, CardHeader } from "@/components/ui/Card";
import { Save, ArrowLeft, Activity, Users, Zap, Ban, CheckCircle2 } from "lucide-react";
import { Save, ArrowLeft, Activity, Users, Zap, Ban, CheckCircle2, Terminal, Wrench } from "lucide-react";
export function AdminPanelPage() {
const { t } = useTranslation();
@@ -22,6 +22,10 @@ export function AdminPanelPage() {
const [error, setError] = useState("");
const [embeddingTest, setEmbeddingTest] = useState<null | { ok: boolean; msg: string }>(null);
const [testingEmbeddings, setTestingEmbeddings] = useState(false);
const [llmTest, setLlmTest] = useState<null | { ok: boolean; msg: string }>(null);
const [testingLlm, setTestingLlm] = useState(false);
const [llmToolsTest, setLlmToolsTest] = useState<null | { ok: boolean; msg: string }>(null);
const [testingLlmTools, setTestingLlmTools] = useState(false);
const [userActionError, setUserActionError] = useState("");
useEffect(() => {
@@ -111,6 +115,92 @@ export function AdminPanelPage() {
}
};
// Build LLM overrides from current form values (excluding masked api_key).
// Used by both testLlm and testLlmTools so the operator can tweak base_url
// / model / api_key in the form and test before saving.
const buildLlmOverrides = (): Record<string, any> => {
const overrides: Record<string, any> = {};
for (const k of ["llm.base_url", "llm.api_key", "llm.model", "llm.request_timeout"]) {
const v = values[k];
if (v !== undefined && v !== null && !(typeof v === "string" && v.includes("***"))) {
overrides[k] = v;
}
}
return overrides;
};
const testLlm = async () => {
setError("");
setTestingLlm(true);
setLlmTest(null);
try {
const r = await adminApi.testLlm(buildLlmOverrides());
if (r.ok) {
setLlmTest({
ok: true,
msg: t("admin.llm_test_ok", {
latency: r.latency_ms ?? 0,
preview: (r.response_preview || "").slice(0, 80),
}),
});
} else {
setLlmTest({
ok: false,
msg: t("admin.llm_test_fail", { error: r.error || `(${r.error_type || "unknown"})` }),
});
}
} catch (err: any) {
setLlmTest({
ok: false,
msg: t("admin.llm_test_fail", { error: err.response?.data?.detail || err.message }),
});
} finally {
setTestingLlm(false);
}
};
const testLlmTools = async () => {
setError("");
setTestingLlmTools(true);
setLlmToolsTest(null);
try {
const r = await adminApi.testLlmTools(buildLlmOverrides());
if (r.ok && r.tool_calls_returned) {
// Model returned a proper tool_call — function-calling works.
setLlmToolsTest({
ok: true,
msg: t("admin.llm_test_tools_ok_with_call", {
name: r.tool_call_name || "?",
args: JSON.stringify(r.tool_call_args || {}),
latency: r.latency_ms ?? 0,
}),
});
} else if (r.ok && !r.tool_calls_returned) {
// Model responded but did NOT use the tool — function-calling is NOT
// supported. The fallback JSON parser will still work, but tool-based
// flows (orchestrator, step-writer) will be unreliable.
setLlmToolsTest({
ok: false,
msg: t("admin.llm_test_tools_ok_no_call", {
text: (r.text || "").slice(0, 120),
}),
});
} else {
setLlmToolsTest({
ok: false,
msg: t("admin.llm_test_fail", { error: r.error || `(${r.error_type || "unknown"})` }),
});
}
} catch (err: any) {
setLlmToolsTest({
ok: false,
msg: t("admin.llm_test_fail", { error: err.response?.data?.detail || err.message }),
});
} finally {
setTestingLlmTools(false);
}
};
const toggleUserActive = async (userId: string, currentActive: boolean) => {
setUserActionError("");
try {
@@ -217,6 +307,30 @@ export function AdminPanelPage() {
{t("admin.streaming")}
</label>
</div>
{/* LLM connectivity tests — runs against current form values
(so the operator can tweak base_url / model / api_key and
test BEFORE saving). */}
<div className="flex flex-wrap items-center gap-2 pt-1">
<Button variant="ghost" onClick={testLlm} disabled={testingLlm}>
<Terminal size={14} className="mr-1" />
{testingLlm ? t("admin.llm_test_testing") : t("admin.llm_test")}
</Button>
<Button variant="ghost" onClick={testLlmTools} disabled={testingLlmTools}>
<Wrench size={14} className="mr-1" />
{testingLlmTools ? t("admin.llm_test_tools_testing") : t("admin.llm_test_tools")}
</Button>
{llmTest && (
<span className={`text-xs ${llmTest.ok ? "text-green-400" : "text-red-400"}`}>
{llmTest.msg}
</span>
)}
{llmToolsTest && (
<span className={`text-xs ${llmToolsTest.ok ? "text-green-400" : "text-red-400"}`}>
{llmToolsTest.msg}
</span>
)}
</div>
</CardBody>
</Card>