fix
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user