fixes
This commit is contained in:
@@ -387,7 +387,7 @@ export function deleteShare(convId: string): void {
|
||||
getDb().query("DELETE FROM shares WHERE conv_id = ?").run(convId);
|
||||
}
|
||||
|
||||
export function getShareByPublic(publicId: string): { conv: ConversationRow; messages: MessageRow[]; authorName: string; sharedAt: number } | null {
|
||||
export function getShareByPublic(publicId: string): { conv: ConversationRow; messages: MessageRow[]; authorName: string; authorAvatarUrl: string; sharedAt: number } | null {
|
||||
const d = getDb();
|
||||
const s = d.query("SELECT * FROM shares WHERE public_id = ?").get(publicId) as { conv_id: string; created_at: number } | null;
|
||||
if (!s) return null;
|
||||
@@ -398,6 +398,7 @@ export function getShareByPublic(publicId: string): { conv: ConversationRow; mes
|
||||
conv,
|
||||
messages: getMessages(conv.id).filter((m) => m.role !== "system"),
|
||||
authorName: author?.display_name || "Someone",
|
||||
authorAvatarUrl: author?.avatar_url || "",
|
||||
sharedAt: s.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,8 +10,16 @@ import type { DriverModel, LLMDriver } from "./types.ts";
|
||||
*/
|
||||
const PRIORITY = ["ollama", "mistral", "glm", "puppeteer-openai", "deepseek", "anthropic", "openai"];
|
||||
|
||||
/** How long the user-facing model list stays in memory before re-hitting upstream. */
|
||||
export const MODEL_CACHE_TTL_MS = 60_000;
|
||||
|
||||
export class DriverRegistry {
|
||||
private drivers: Map<string, LLMDriver>;
|
||||
// In-memory model list cache: user requests are served from here so we
|
||||
// don't hit Ollama (/api/tags) on every login. Refreshed at most once
|
||||
// per MODEL_CACHE_TTL_MS; admins can force a refresh.
|
||||
private modelsCache: { models: DriverModel[]; at: number } | null = null;
|
||||
private modelsInflight: Promise<DriverModel[]> | null = null;
|
||||
|
||||
constructor() {
|
||||
const all: LLMDriver[] = [
|
||||
@@ -49,6 +57,36 @@ export class DriverRegistry {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached variant of listAllModels for user-facing requests.
|
||||
* - Serves the in-memory list when fresher than MODEL_CACHE_TTL_MS.
|
||||
* - Otherwise refetches upstream once (concurrent callers share the
|
||||
* same in-flight promise instead of hammering Ollama).
|
||||
* - `force=true` bypasses the cache (admin "Ollama models" view).
|
||||
*/
|
||||
async listAllModelsCached(force = false): Promise<DriverModel[]> {
|
||||
const now = Date.now();
|
||||
if (!force && this.modelsCache && now - this.modelsCache.at < MODEL_CACHE_TTL_MS) {
|
||||
return this.modelsCache.models;
|
||||
}
|
||||
if (!force && this.modelsInflight) return this.modelsInflight;
|
||||
const p = this.listAllModels().then((models) => {
|
||||
this.modelsCache = { models, at: Date.now() };
|
||||
return models;
|
||||
}).finally(() => {
|
||||
if (this.modelsInflight === p) this.modelsInflight = null;
|
||||
});
|
||||
// Only share the promise for non-forced loads; a forced admin refresh
|
||||
// always goes upstream even if a user load is in flight.
|
||||
if (!force) this.modelsInflight = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Drop the cached model list (e.g. after an admin pull/delete). */
|
||||
invalidateModelsCache(): void {
|
||||
this.modelsCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a fully-qualified "driver:model" id.
|
||||
* "ollama:llama3.1:8b" -> driver "ollama", model "llama3.1:8b".
|
||||
|
||||
+16
-3
@@ -198,6 +198,7 @@ const server = Bun.serve({
|
||||
topic: s.conv.topic,
|
||||
model: s.conv.model,
|
||||
authorName: s.authorName,
|
||||
authorAvatarUrl: s.authorAvatarUrl,
|
||||
sharedAt: s.sharedAt,
|
||||
messages: s.messages.map((m) => ({ role: m.role, content: m.content })),
|
||||
});
|
||||
@@ -768,16 +769,21 @@ const server = Bun.serve({
|
||||
// Passthrough of the NDJSON progress lines (status/digest/completed/total).
|
||||
const dec = new TextDecoder();
|
||||
let buf = "";
|
||||
let pulledOk = true;
|
||||
for await (const chunk of res.body) {
|
||||
if (req.signal.aborted) break;
|
||||
if (req.signal.aborted) { pulledOk = false; break; }
|
||||
buf += dec.decode(chunk, { stream: true });
|
||||
let nl: number;
|
||||
while ((nl = buf.indexOf("\n")) >= 0) {
|
||||
const line = buf.slice(0, nl).trim();
|
||||
buf = buf.slice(nl + 1);
|
||||
if (line && !send(JSON.parse(line))) break;
|
||||
if (line && !send(JSON.parse(line))) { pulledOk = false; break; }
|
||||
}
|
||||
if (!pulledOk) break;
|
||||
}
|
||||
// A new model landed on the Ollama host: drop the cached list
|
||||
// so the next user request picks it up (subject to TTL).
|
||||
if (pulledOk && !req.signal.aborted) registry.invalidateModelsCache();
|
||||
send({ status: "done" });
|
||||
} catch (e) {
|
||||
if (!req.signal.aborted && !clientGone) send({ error: (e as Error).message });
|
||||
@@ -812,6 +818,7 @@ const server = Bun.serve({
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
if (!res.ok) return json({ error: `ollama /api/delete failed: ${res.status}` }, 502);
|
||||
registry.invalidateModelsCache();
|
||||
return json({ ok: true });
|
||||
} catch (e) {
|
||||
return json({ error: (e as Error).message }, 502);
|
||||
@@ -967,7 +974,13 @@ const server = Bun.serve({
|
||||
}
|
||||
|
||||
if (path === "/api/models" && req.method === "GET") {
|
||||
const models = await registry.listAllModels();
|
||||
// User-facing list is served from an in-memory cache (TTL 60s) so
|
||||
// every login doesn't hit Ollama. Admins pass ?refresh=1 to force
|
||||
// a fresh upstream fetch (Ollama models view).
|
||||
const wantRefresh = /^(1|true|yes)$/i.test(
|
||||
url.searchParams.get("refresh") ?? url.searchParams.get("force") ?? "",
|
||||
);
|
||||
const models = await registry.listAllModelsCached(wantRefresh && admin);
|
||||
return json({ models });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user