feat(agent): refresh assistant model catalog
This commit is contained in:
@@ -3,15 +3,22 @@ import { resolveApiAuth } from "@/lib/api-auth";
|
||||
import { getAssistantPreferences } from "@/lib/assistant-preference";
|
||||
import { listLlmModels, resolveAssistantModel } from "@/lib/llm/models";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function noStore(response: Response): Response {
|
||||
response.headers.set("Cache-Control", "no-store");
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = await resolveApiAuth(request);
|
||||
if (!auth?.userId) {
|
||||
return apiError("Unauthorized", 401);
|
||||
return noStore(apiError("Unauthorized", 401));
|
||||
}
|
||||
|
||||
const assistant = await getAssistantPreferences(auth.userId);
|
||||
if (!assistant.enabled) {
|
||||
return apiError("Assistant not enabled", 403);
|
||||
return noStore(apiError("Assistant not enabled", 403));
|
||||
}
|
||||
|
||||
const modelList = await listLlmModels();
|
||||
@@ -22,10 +29,12 @@ export async function GET(request: Request) {
|
||||
models: modelList.models,
|
||||
});
|
||||
|
||||
return apiJson({
|
||||
models: modelList.models,
|
||||
selectedModel: resolved.model,
|
||||
fallbackModel: modelList.fallbackModel,
|
||||
degraded: modelList.degraded,
|
||||
});
|
||||
return noStore(
|
||||
apiJson({
|
||||
models: modelList.models,
|
||||
selectedModel: resolved.model,
|
||||
fallbackModel: modelList.fallbackModel,
|
||||
degraded: modelList.degraded,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,12 @@ export async function listLlmModels(options?: {
|
||||
const headers: Record<string, string> = {};
|
||||
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
||||
|
||||
const response = await fetchImpl(`${config.baseUrl.replace(/\/$/, "")}/models`, {
|
||||
const modelsUrl = new URL(`${config.baseUrl.replace(/\/$/, "")}/models`);
|
||||
if (fallbackModel === "uncensored") {
|
||||
modelsUrl.searchParams.set("type", "uncensored");
|
||||
}
|
||||
|
||||
const response = await fetchImpl(modelsUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, useTransition } from "react";
|
||||
import { ChevronDown, ImagePlus, Loader2, Mic, Send, Square } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState, useTransition } from "react";
|
||||
import { ChevronDown, ImagePlus, Loader2, Mic, RefreshCw, Send, Square } from "lucide-react";
|
||||
import { setAssistantModel } from "@/app/settings/assistant-actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -87,34 +87,44 @@ export function AssistantPanel({ configured, userId, assistantName, assistantMod
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const loadModels = useCallback(async (options?: { refresh?: boolean; signal?: AbortSignal }) => {
|
||||
if (options?.signal?.aborted) return;
|
||||
|
||||
async function loadModels() {
|
||||
setModelsLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/agent/models");
|
||||
if (!response.ok) throw new Error("Model discovery unavailable");
|
||||
const payload = (await response.json()) as ModelsResponse;
|
||||
if (cancelled) return;
|
||||
setModelOptions(payload.models);
|
||||
setSelectedModel(payload.selectedModel);
|
||||
setFallbackModel(payload.fallbackModel);
|
||||
setModelsDegraded(payload.degraded);
|
||||
} catch {
|
||||
if (cancelled) return;
|
||||
setModelsDegraded(true);
|
||||
} finally {
|
||||
if (!cancelled) setModelsLoading(false);
|
||||
}
|
||||
setModelsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/agent/models${options?.refresh ? "?refresh=1" : ""}`, {
|
||||
cache: "no-store",
|
||||
signal: options?.signal,
|
||||
});
|
||||
if (!response.ok) throw new Error("Model discovery unavailable");
|
||||
const payload = (await response.json()) as ModelsResponse;
|
||||
if (options?.signal?.aborted) return;
|
||||
setModelOptions(payload.models);
|
||||
setSelectedModel(payload.selectedModel);
|
||||
setFallbackModel(payload.fallbackModel);
|
||||
setModelsDegraded(payload.degraded);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === "AbortError") return;
|
||||
setModelsDegraded(true);
|
||||
setError("Model discovery unavailable");
|
||||
} finally {
|
||||
if (!options?.signal?.aborted) setModelsLoading(false);
|
||||
}
|
||||
|
||||
void loadModels();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
queueMicrotask(() => {
|
||||
void loadModels({ signal: controller.signal });
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [loadModels]);
|
||||
|
||||
function scrollToBottom() {
|
||||
requestAnimationFrame(() => {
|
||||
const node = listRef.current;
|
||||
@@ -271,6 +281,20 @@ export function AssistantPanel({ configured, userId, assistantName, assistantMod
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label="Refresh assistant models"
|
||||
disabled={modelsLoading || savingModel || isPending}
|
||||
onClick={() => void loadModels({ refresh: true })}
|
||||
>
|
||||
{modelsLoading ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
{messages.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user