返回 presentation-ai
route.ts
根目录 / src / app / api / presentation / local-models / route.ts
1 import { createLogger } from "@/lib/observability/logger";
2 import { auth } from "@/server/auth";
3 import { NextResponse } from "next/server";
4
5 interface LocalModelInfo {
6 id: string;
7 name: string;
8 provider: "ollama" | "lmstudio";
9 }
10
11 interface OllamaTagsResponse {
12 models?: Array<{ name?: string }>;
13 }
14
15 interface LMStudioNativeResponse {
16 models?: Array<{
17 key?: string;
18 display_name?: string;
19 loaded_instances?: Array<{ id?: string }>;
20 }>;
21 }
22
23 interface LMStudioOpenAIResponse {
24 data?: Array<{ id?: string }>;
25 }
26
27 const routeLogger = createLogger("api:presentation-local-models");
28 const OLLAMA_TAGS_URL = "http://localhost:11434/api/tags";
29 const LM_STUDIO_NATIVE_MODELS_URL = "http://localhost:1234/api/v1/models";
30 const LM_STUDIO_OPENAI_MODELS_URL = "http://localhost:1234/v1/models";
31 const LOCAL_FETCH_TIMEOUT_MS = 2_500;
32
33 function createTimeoutSignal(timeoutMs: number): AbortSignal {
34 const controller = new AbortController();
35 const timeout = setTimeout(() => controller.abort(), timeoutMs);
36 controller.signal.addEventListener("abort", () => clearTimeout(timeout), {
37 once: true,
38 });
39 return controller.signal;
40 }
41
42 function dedupeModels(models: LocalModelInfo[]): LocalModelInfo[] {
43 const seen = new Set<string>();
44
45 return models.filter((model) => {
46 if (seen.has(model.id)) {
47 return false;
48 }
49
50 seen.add(model.id);
51 return true;
52 });
53 }
54
55 async function fetchOllamaModels(): Promise<LocalModelInfo[]> {
56 try {
57 const response = await fetch(OLLAMA_TAGS_URL, {
58 cache: "no-store",
59 signal: createTimeoutSignal(LOCAL_FETCH_TIMEOUT_MS),
60 });
61
62 if (!response.ok) {
63 throw new Error(`Ollama responded with ${response.status}`);
64 }
65
66 const data = (await response.json()) as OllamaTagsResponse;
67 return (data.models ?? [])
68 .map((model) => model.name?.trim())
69 .filter((name): name is string => Boolean(name))
70 .map((name) => ({
71 id: `ollama-${name}`,
72 name,
73 provider: "ollama" as const,
74 }));
75 } catch (error) {
76 routeLogger.warn("Failed to fetch Ollama models", {
77 error: error instanceof Error ? error.message : String(error),
78 });
79 return [];
80 }
81 }
82
83 async function fetchLMStudioModels(): Promise<LocalModelInfo[]> {
84 try {
85 const response = await fetch(LM_STUDIO_NATIVE_MODELS_URL, {
86 cache: "no-store",
87 signal: createTimeoutSignal(LOCAL_FETCH_TIMEOUT_MS),
88 });
89
90 if (!response.ok) {
91 throw new Error(`LM Studio native endpoint responded with ${response.status}`);
92 }
93
94 const data = (await response.json()) as LMStudioNativeResponse;
95 const models = (data.models ?? []).flatMap((model) => {
96 const loadedInstances = (model.loaded_instances ?? [])
97 .map((instance) => instance.id?.trim())
98 .filter((id): id is string => Boolean(id));
99
100 if (loadedInstances.length === 0) {
101 return [];
102 }
103
104 const modelKey = model.key?.trim();
105 const displayName = model.display_name?.trim();
106
107 return loadedInstances.map((instanceId) => ({
108 id: `lmstudio-${instanceId}`,
109 name: displayName || modelKey || instanceId,
110 provider: "lmstudio" as const,
111 }));
112 });
113
114 if (models.length > 0) {
115 return dedupeModels(models);
116 }
117 } catch (error) {
118 routeLogger.warn("Failed to fetch LM Studio native model list", {
119 error: error instanceof Error ? error.message : String(error),
120 });
121 }
122
123 try {
124 const response = await fetch(LM_STUDIO_OPENAI_MODELS_URL, {
125 cache: "no-store",
126 signal: createTimeoutSignal(LOCAL_FETCH_TIMEOUT_MS),
127 });
128
129 if (!response.ok) {
130 throw new Error(`LM Studio OpenAI endpoint responded with ${response.status}`);
131 }
132
133 const data = (await response.json()) as LMStudioOpenAIResponse;
134 return dedupeModels(
135 (data.data ?? [])
136 .map((model) => model.id?.trim())
137 .filter((id): id is string => Boolean(id))
138 .map((id) => ({
139 id: `lmstudio-${id}`,
140 name: id,
141 provider: "lmstudio" as const,
142 })),
143 );
144 } catch (error) {
145 routeLogger.warn("Failed to fetch LM Studio OpenAI-compatible model list", {
146 error: error instanceof Error ? error.message : String(error),
147 });
148 return [];
149 }
150 }
151
152 export async function GET() {
153 const session = await auth();
154 if (!session) {
155 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
156 }
157
158 const [ollamaModels, lmStudioModels] = await Promise.all([
159 fetchOllamaModels(),
160 fetchLMStudioModels(),
161 ]);
162
163 return NextResponse.json(
164 {
165 models: dedupeModels([...ollamaModels, ...lmStudioModels]),
166 },
167 {
168 headers: {
169 "Cache-Control": "no-store",
170 },
171 },
172 );
173 }
174
174 lines TYPESCRIPT