@kevin5251984/guild 0.2.19 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ import type { ModelReasoning } from "@guild/protocol";
2
+ import { guildUserAgent } from "./version.ts";
3
+
4
+ const MODELS_DEV = "https://models.dev/api.json";
5
+ const OPENROUTER_MODELS = "https://openrouter.ai/api/v1/models";
6
+ const TTL_MS = 6 * 60 * 60 * 1000;
7
+ const FETCH_MS = 4_000;
8
+
9
+ const DEV_PROVIDER: Record<string, string> = {
10
+ xai: "xai",
11
+ "xai-oauth": "xai",
12
+ openai: "openai",
13
+ "openai-codex": "openai",
14
+ anthropic: "anthropic",
15
+ "anthropic-oauth": "anthropic",
16
+ "github-copilot": "github-copilot",
17
+ "opencode-free": "opencode",
18
+ openrouter: "openrouter",
19
+ "openrouter-oauth": "openrouter",
20
+ };
21
+
22
+ const OPENROUTER_PREFIX: Record<string, string> = {
23
+ xai: "x-ai",
24
+ openai: "openai",
25
+ anthropic: "anthropic",
26
+ "opencode-free": "opencode",
27
+ "github-copilot": "github-copilot",
28
+ };
29
+
30
+ /** Sort key only — never used as the displayed list. */
31
+ const EFFORT_RANK = [
32
+ "none",
33
+ "minimal",
34
+ "low",
35
+ "medium",
36
+ "high",
37
+ "xhigh",
38
+ "max",
39
+ ];
40
+
41
+ type CatalogMaps = {
42
+ at: number;
43
+ dev: Map<string, ModelReasoning>;
44
+ openrouter: Map<string, ModelReasoning>;
45
+ gatewayEfforts: string[];
46
+ };
47
+
48
+ let maps: CatalogMaps | null = null;
49
+
50
+ export function parseEffortList(raw: unknown): string[] {
51
+ if (!Array.isArray(raw)) return [];
52
+ const out: string[] = [];
53
+ for (const item of raw) {
54
+ if (typeof item !== "string") continue;
55
+ const key = item.trim().toLowerCase();
56
+ if (!/^[a-z][a-z0-9_-]{0,31}$/.test(key) || out.includes(key)) continue;
57
+ out.push(key);
58
+ }
59
+ return out;
60
+ }
61
+
62
+ export function sanitizeEffort(raw: unknown): string | undefined {
63
+ if (typeof raw !== "string") return undefined;
64
+ const key = raw.trim().toLowerCase();
65
+ if (!/^[a-z][a-z0-9_-]{0,31}$/.test(key)) return undefined;
66
+ return key;
67
+ }
68
+
69
+ export function fromModelsDevModel(raw: unknown): ModelReasoning | undefined {
70
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
71
+ const row = raw as Record<string, unknown>;
72
+ if (row.reasoning === false) return undefined;
73
+ const options = Array.isArray(row.reasoning_options)
74
+ ? row.reasoning_options
75
+ : [];
76
+ let efforts: string[] = [];
77
+ let supportsMaxTokens = false;
78
+ for (const opt of options) {
79
+ if (!opt || typeof opt !== "object" || Array.isArray(opt)) continue;
80
+ const rec = opt as Record<string, unknown>;
81
+ if (rec.type === "effort") efforts = parseEffortList(rec.values);
82
+ if (rec.type === "budget_tokens") supportsMaxTokens = true;
83
+ }
84
+ if (row.reasoning !== true && !efforts.length && !supportsMaxTokens) {
85
+ return undefined;
86
+ }
87
+ const spec: ModelReasoning = {};
88
+ if (efforts.length) spec.supportedEfforts = efforts;
89
+ if (supportsMaxTokens) spec.supportsMaxTokens = true;
90
+ spec.defaultEnabled = true;
91
+ if (efforts.length) spec.mandatory = !efforts.includes("none");
92
+ if (efforts.includes("high")) spec.defaultEffort = "high";
93
+ else if (efforts.includes("medium")) spec.defaultEffort = "medium";
94
+ else if (efforts.length) spec.defaultEffort = efforts[0];
95
+ return spec;
96
+ }
97
+
98
+ export function fromOpenRouterModel(raw: unknown): ModelReasoning | undefined {
99
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
100
+ const row = raw as Record<string, unknown>;
101
+ const block = row.reasoning;
102
+ if (block === undefined) return undefined;
103
+ if (!block || typeof block !== "object" || Array.isArray(block)) {
104
+ return undefined;
105
+ }
106
+ const rec = block as Record<string, unknown>;
107
+ const spec: ModelReasoning = {};
108
+ if (rec.supported_efforts === null) {
109
+ spec.supportedEfforts = undefined;
110
+ } else {
111
+ const efforts = parseEffortList(rec.supported_efforts);
112
+ if (efforts.length) spec.supportedEfforts = efforts;
113
+ }
114
+ const def = sanitizeEffort(rec.default_effort);
115
+ if (def) spec.defaultEffort = def;
116
+ if (typeof rec.mandatory === "boolean") spec.mandatory = rec.mandatory;
117
+ if (typeof rec.default_enabled === "boolean") {
118
+ spec.defaultEnabled = rec.default_enabled;
119
+ }
120
+ if (rec.supports_max_tokens === true) spec.supportsMaxTokens = true;
121
+ if (
122
+ !spec.supportedEfforts &&
123
+ spec.defaultEffort === undefined &&
124
+ spec.mandatory === undefined &&
125
+ spec.supportsMaxTokens === undefined
126
+ ) {
127
+ return { defaultEnabled: spec.defaultEnabled ?? true };
128
+ }
129
+ return spec;
130
+ }
131
+
132
+ function indexDev(data: unknown): Map<string, ModelReasoning> {
133
+ const out = new Map<string, ModelReasoning>();
134
+ if (!data || typeof data !== "object") return out;
135
+ for (const [pid, prov] of Object.entries(data as Record<string, unknown>)) {
136
+ if (!prov || typeof prov !== "object") continue;
137
+ const models = (prov as { models?: Record<string, unknown> }).models;
138
+ if (!models || typeof models !== "object") continue;
139
+ for (const [mid, model] of Object.entries(models)) {
140
+ const spec = fromModelsDevModel(model);
141
+ if (!spec) continue;
142
+ out.set(`${pid}/${mid}`, spec);
143
+ const bare = mid.split("/").pop() || mid;
144
+ if (bare !== mid) out.set(`${pid}/${bare}`, spec);
145
+ }
146
+ }
147
+ return out;
148
+ }
149
+
150
+ function indexOpenRouter(data: unknown): {
151
+ map: Map<string, ModelReasoning>;
152
+ gateway: string[];
153
+ } {
154
+ const map = new Map<string, ModelReasoning>();
155
+ const seen = new Set<string>();
156
+ const rows = Array.isArray(data)
157
+ ? data
158
+ : data && typeof data === "object" && Array.isArray((data as { data?: unknown }).data)
159
+ ? ((data as { data: unknown[] }).data)
160
+ : [];
161
+ for (const row of rows) {
162
+ const spec = fromOpenRouterModel(row);
163
+ if (!spec) continue;
164
+ const id =
165
+ row && typeof row === "object" && typeof (row as { id?: unknown }).id === "string"
166
+ ? (row as { id: string }).id
167
+ : "";
168
+ if (!id) continue;
169
+ map.set(id, spec);
170
+ const bare = id.split("/").pop() || id;
171
+ if (bare !== id && !map.has(bare)) map.set(bare, spec);
172
+ for (const effort of spec.supportedEfforts ?? []) seen.add(effort);
173
+ }
174
+ const gateway = EFFORT_RANK.filter((key) => seen.has(key));
175
+ for (const key of seen) {
176
+ if (!gateway.includes(key)) gateway.push(key);
177
+ }
178
+ return { map, gateway };
179
+ }
180
+
181
+ export function setReasoningCatalogForTests(input: {
182
+ dev?: unknown;
183
+ openrouter?: unknown;
184
+ }): void {
185
+ const openrouter = indexOpenRouter(input.openrouter ?? { data: [] });
186
+ maps = {
187
+ at: Date.now(),
188
+ dev: indexDev(input.dev ?? {}),
189
+ openrouter: openrouter.map,
190
+ gatewayEfforts: openrouter.gateway,
191
+ };
192
+ }
193
+
194
+ export function resetReasoningCatalog(): void {
195
+ maps = null;
196
+ }
197
+
198
+ function skipLiveFetch(): boolean {
199
+ return Boolean(
200
+ process.env.NODE_TEST_CONTEXT ||
201
+ process.argv.some((arg) => arg === "--test" || arg.includes("--test=")),
202
+ );
203
+ }
204
+
205
+ export async function refreshReasoningCatalog(
206
+ force = false,
207
+ fetcher: typeof fetch = fetch,
208
+ ): Promise<void> {
209
+ if (!force && maps && Date.now() - maps.at < TTL_MS) return;
210
+ if (!force && skipLiveFetch() && maps) return;
211
+ if (skipLiveFetch() && !force) return;
212
+ const ctrl = AbortSignal.timeout(FETCH_MS);
213
+ const [devRes, orRes] = await Promise.allSettled([
214
+ fetcher(MODELS_DEV, {
215
+ headers: { accept: "application/json", "user-agent": guildUserAgent() },
216
+ signal: ctrl,
217
+ }),
218
+ fetcher(OPENROUTER_MODELS, {
219
+ headers: { accept: "application/json", "user-agent": guildUserAgent() },
220
+ signal: ctrl,
221
+ }),
222
+ ]);
223
+ let dev = maps?.dev ?? new Map<string, ModelReasoning>();
224
+ let openrouter = maps?.openrouter ?? new Map<string, ModelReasoning>();
225
+ let gateway = maps?.gatewayEfforts ?? [];
226
+ let got = false;
227
+ if (devRes.status === "fulfilled" && devRes.value.ok) {
228
+ try {
229
+ dev = indexDev(await devRes.value.json());
230
+ got = true;
231
+ } catch {
232
+ /* keep previous */
233
+ }
234
+ }
235
+ if (orRes.status === "fulfilled" && orRes.value.ok) {
236
+ try {
237
+ const indexed = indexOpenRouter(await orRes.value.json());
238
+ openrouter = indexed.map;
239
+ gateway = indexed.gateway;
240
+ got = true;
241
+ } catch {
242
+ /* keep previous */
243
+ }
244
+ }
245
+ if (!got && !maps) return;
246
+ if (!got) return;
247
+ maps = {
248
+ at: Date.now(),
249
+ dev,
250
+ openrouter,
251
+ gatewayEfforts: gateway,
252
+ };
253
+ }
254
+
255
+ function fillNullEfforts(spec: ModelReasoning): ModelReasoning {
256
+ if (spec.supportedEfforts?.length) return spec;
257
+ if (!maps?.gatewayEfforts.length) return spec;
258
+ return { ...spec, supportedEfforts: [...maps.gatewayEfforts] };
259
+ }
260
+
261
+ export function reasoningFor(
262
+ providerId: string,
263
+ modelId: string,
264
+ ): ModelReasoning | undefined {
265
+ if (!maps) return undefined;
266
+ const provider = String(providerId || "").trim().toLowerCase();
267
+ const model = String(modelId || "").trim();
268
+ if (!provider || !model) return undefined;
269
+ const bare = model.split("/").pop() || model;
270
+ const devId = DEV_PROVIDER[provider] || provider.replace(/-oauth$/, "");
271
+
272
+ if (devId === "openrouter") {
273
+ const hit =
274
+ maps.openrouter.get(model) ||
275
+ maps.openrouter.get(bare) ||
276
+ maps.openrouter.get(model.toLowerCase());
277
+ return hit ? fillNullEfforts(hit) : undefined;
278
+ }
279
+
280
+ const devHit =
281
+ maps.dev.get(`${devId}/${model}`) ||
282
+ maps.dev.get(`${devId}/${bare}`) ||
283
+ maps.dev.get(`${devId}/${bare.toLowerCase()}`);
284
+ if (devHit) return devHit;
285
+
286
+ const prefix = OPENROUTER_PREFIX[devId] || OPENROUTER_PREFIX[provider];
287
+ if (prefix) {
288
+ const slug = `${prefix}/${bare}`;
289
+ const orHit = maps.openrouter.get(slug) || maps.openrouter.get(model);
290
+ if (orHit) return fillNullEfforts(orHit);
291
+ }
292
+ return undefined;
293
+ }
294
+
295
+ export function attachReasoning(
296
+ providerId: string,
297
+ models: { id: string; name?: string; reasoning?: ModelReasoning }[],
298
+ ): { id: string; name?: string; reasoning?: ModelReasoning }[] {
299
+ return models.map((model) => {
300
+ const spec = reasoningFor(providerId, model.id);
301
+ if (!spec) return model;
302
+ return { ...model, reasoning: spec };
303
+ });
304
+ }
305
+
306
+ export function clampEffort(
307
+ want: string | undefined,
308
+ spec: ModelReasoning | undefined,
309
+ fast = false,
310
+ ): string | undefined {
311
+ const efforts = spec?.supportedEfforts;
312
+ if (!efforts?.length) return undefined;
313
+ const usable = spec?.mandatory
314
+ ? efforts.filter((key) => key !== "none")
315
+ : efforts;
316
+ if (!usable.length) return undefined;
317
+ if (fast) {
318
+ if (usable.includes("low")) return "low";
319
+ if (usable.includes("minimal")) return "minimal";
320
+ const ranked = EFFORT_RANK.filter((key) => usable.includes(key));
321
+ return ranked[0] || usable[usable.length - 1];
322
+ }
323
+ const picked = sanitizeEffort(want);
324
+ if (picked && usable.includes(picked)) return picked;
325
+ if (spec?.defaultEffort && usable.includes(spec.defaultEffort)) {
326
+ return spec.defaultEffort;
327
+ }
328
+ return usable.includes("high")
329
+ ? "high"
330
+ : usable.includes("medium")
331
+ ? "medium"
332
+ : usable[0];
333
+ }
334
+
335
+ export function reasoningPayload(
336
+ providerId: string,
337
+ baseUrl: string,
338
+ effort: string | undefined,
339
+ ): Record<string, unknown> {
340
+ if (!effort) return {};
341
+ const viaOpenRouter =
342
+ providerId.includes("openrouter") ||
343
+ /openrouter\.ai/i.test(baseUrl);
344
+ if (viaOpenRouter) return { reasoning: { effort } };
345
+ return { reasoning_effort: effort, reasoning: { effort } };
346
+ }
package/src/router.ts CHANGED
@@ -24,11 +24,14 @@ import {
24
24
  getBotDetail,
25
25
  getLiveTurn,
26
26
  abortLiveTurn,
27
+ pauseLiveTurn,
28
+ continueLiveTurn,
27
29
  healthPayload,
28
30
  importSkills,
29
31
  mergeModelsFile,
30
32
  publicModels,
31
33
  refreshOpenCodeFreeCatalog,
34
+ refreshReasoningCatalog,
32
35
  listBench,
33
36
  listLibrary,
34
37
  listMcpServers,
@@ -102,11 +105,107 @@ const LIBRARY_KINDS = new Set<LibraryKind>([
102
105
  "subagents",
103
106
  ]);
104
107
 
105
- const CORS_HEADERS = {
106
- "access-control-allow-origin": "*",
107
- "access-control-allow-methods": "GET, POST, PATCH, PUT, DELETE, OPTIONS",
108
- "access-control-allow-headers": "content-type, authorization",
109
- } as const;
108
+ /**
109
+ * Same-origin guard. The hall UI is served by this daemon (`http://127.0.0.1:7420/`
110
+ * and the Tailscale address), so it never needs CORS. A request that carries an
111
+ * `Origin` from somewhere else is a foreign page trying to read local files
112
+ * (`/host/read`), so it is refused before any route runs.
113
+ */
114
+ const LOCAL_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
115
+ /** Tailscale MagicDNS names: `machine.tailnet.ts.net`. */
116
+ const MAGIC_DNS_SUFFIX = ".ts.net";
117
+
118
+ function headerLine(value: string | string[] | undefined): string {
119
+ const first = Array.isArray(value) ? value[0] : value;
120
+ return typeof first === "string" ? first.trim() : "";
121
+ }
122
+
123
+ function normalizeHost(host: string): string {
124
+ const lower = host.trim().toLowerCase().replace(/\.$/, "");
125
+ return LOCAL_HOSTS.has(lower) ? "127.0.0.1" : lower;
126
+ }
127
+
128
+ /** Host header is an authority, not a URL: `127.0.0.1:7420`, `[::1]:7420`, `bot.local`. */
129
+ function hostPortOf(authority: string): { host: string; port: string } {
130
+ const value = authority.trim();
131
+ if (!value) return { host: "", port: "" };
132
+ if (value.startsWith("[")) {
133
+ const end = value.indexOf("]");
134
+ const host = end === -1 ? value : value.slice(0, end + 1);
135
+ const rest = end === -1 ? "" : value.slice(end + 1);
136
+ const colon = rest.indexOf(":");
137
+ return { host, port: colon === -1 ? "" : rest.slice(colon + 1) };
138
+ }
139
+ const colon = value.indexOf(":");
140
+ if (colon === -1) return { host: value, port: "" };
141
+ return { host: value.slice(0, colon), port: value.slice(colon + 1) };
142
+ }
143
+
144
+ /** Dotted-quad octets, or null when this is not an IPv4 literal. */
145
+ function ipv4Octets(host: string): number[] | null {
146
+ const parts = host.split(".");
147
+ if (parts.length !== 4) return null;
148
+ const octets: number[] = [];
149
+ for (const part of parts) {
150
+ if (!/^\d{1,3}$/.test(part)) return null;
151
+ const value = Number(part);
152
+ if (!Number.isInteger(value) || value > 255) return null;
153
+ octets.push(value);
154
+ }
155
+ return octets;
156
+ }
157
+
158
+ /**
159
+ * True only for authorities that cannot be a public name: loopback, RFC1918,
160
+ * CGNAT / Tailscale 100.64/10, and MagicDNS `*.ts.net`. A rebinding page
161
+ * (`evil.com` resolving to 127.0.0.1) matches Origin against Host, so the
162
+ * authority itself has to be checked too.
163
+ */
164
+ export function isLocalAuthority(raw: string): boolean {
165
+ let host = String(raw || "").trim().toLowerCase().replace(/\.$/, "");
166
+ if (!host) return false;
167
+ if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
168
+ if (host === "::1" || host === "0:0:0:0:0:0:0:1") return true;
169
+ if (LOCAL_HOSTS.has(host)) return true;
170
+ const octets = ipv4Octets(host);
171
+ if (octets) {
172
+ const [first, second] = octets as [number, number, number, number];
173
+ if (first === 127) return true; // 127/8 loopback
174
+ if (first === 10) return true; // RFC1918
175
+ if (first === 172 && second >= 16 && second <= 31) return true;
176
+ if (first === 192 && second === 168) return true;
177
+ if (first === 100 && second >= 64 && second <= 127) return true; // CGNAT
178
+ return false;
179
+ }
180
+ return host.endsWith(MAGIC_DNS_SUFFIX);
181
+ }
182
+
183
+ /** True when there is no Origin (curl, Node fetch, same-origin opaque) or it matches Host. */
184
+ export function sameOrigin(req: IncomingMessage): boolean {
185
+ const raw = headerLine(req.headers.origin);
186
+ if (!raw) return true;
187
+ let origin: URL;
188
+ try {
189
+ origin = new URL(raw);
190
+ } catch {
191
+ return false;
192
+ }
193
+ const secure = origin.protocol === "https:";
194
+ if (!secure && origin.protocol !== "http:") return false;
195
+ const host = headerLine(req.headers.host);
196
+ if (!host) return false;
197
+ const wanted = hostPortOf(host);
198
+ // Origin == Host is not enough: DNS rebinding makes a public name resolve to
199
+ // this machine, so both authorities must be loopback / private.
200
+ if (!isLocalAuthority(wanted.host) || !isLocalAuthority(origin.hostname)) {
201
+ return false;
202
+ }
203
+ return (
204
+ normalizeHost(wanted.host) === normalizeHost(origin.hostname) &&
205
+ (wanted.port || (secure ? "443" : "80")) ===
206
+ (origin.port || (secure ? "443" : "80"))
207
+ );
208
+ }
110
209
 
111
210
  function send(
112
211
  res: ServerResponse,
@@ -119,7 +218,6 @@ function send(
119
218
  "content-type": type,
120
219
  "content-length": Buffer.byteLength(body),
121
220
  "cache-control": "no-store",
122
- ...CORS_HEADERS,
123
221
  ...extra,
124
222
  });
125
223
  res.end(body);
@@ -175,7 +273,8 @@ function modelRefFrom(value: unknown): ModelRef | null {
175
273
  const provider = str(rec, "provider").trim();
176
274
  const model = str(rec, "model").trim();
177
275
  if (!provider || !model) return null;
178
- return { provider, model };
276
+ const reasoning = str(rec, "reasoning").trim();
277
+ return reasoning ? { provider, model, reasoning } : { provider, model };
179
278
  }
180
279
 
181
280
  function strList(record: Record<string, unknown>, key: string): string[] {
@@ -261,8 +360,12 @@ export async function handleRequest(
261
360
  const path = pathname(req);
262
361
 
263
362
  try {
363
+ if (!sameOrigin(req)) {
364
+ throw new StoreError(403, "cross-origin refused");
365
+ }
366
+
264
367
  if (method === "OPTIONS") {
265
- res.writeHead(204, CORS_HEADERS);
368
+ res.writeHead(204);
266
369
  res.end();
267
370
  return;
268
371
  }
@@ -311,7 +414,6 @@ export async function handleRequest(
311
414
  "content-type": type,
312
415
  "content-length": bytes.length,
313
416
  "cache-control": "private, max-age=86400",
314
- ...CORS_HEADERS,
315
417
  });
316
418
  res.end(bytes);
317
419
  return;
@@ -610,6 +712,66 @@ export async function handleRequest(
610
712
  return;
611
713
  }
612
714
 
715
+ const channelPause = path.match(/^\/channels\/([^/]+)\/pause$/);
716
+ if (channelPause && method === "POST") {
717
+ const body = asRecord(await readJson(req));
718
+ json(
719
+ res,
720
+ 200,
721
+ pauseLiveTurn(
722
+ store,
723
+ decodeURIComponent(channelPause[1]),
724
+ str(body, "botId") || undefined,
725
+ ),
726
+ );
727
+ return;
728
+ }
729
+ const dmPause = path.match(/^\/dms\/([^/]+)\/pause$/);
730
+ if (dmPause && method === "POST") {
731
+ const room = openDm(store, decodeURIComponent(dmPause[1]));
732
+ const body = asRecord(await readJson(req));
733
+ json(
734
+ res,
735
+ 200,
736
+ pauseLiveTurn(store, room.id, str(body, "botId") || undefined),
737
+ );
738
+ return;
739
+ }
740
+
741
+ const channelContinue = path.match(/^\/channels\/([^/]+)\/continue$/);
742
+ if (channelContinue && method === "POST") {
743
+ const body = asRecord(await readJson(req));
744
+ json(
745
+ res,
746
+ 200,
747
+ await continueLiveTurn(
748
+ store,
749
+ decodeURIComponent(channelContinue[1]),
750
+ str(body, "botId"),
751
+ env,
752
+ extras,
753
+ ),
754
+ );
755
+ return;
756
+ }
757
+ const dmContinue = path.match(/^\/dms\/([^/]+)\/continue$/);
758
+ if (dmContinue && method === "POST") {
759
+ const room = openDm(store, decodeURIComponent(dmContinue[1]));
760
+ const body = asRecord(await readJson(req));
761
+ json(
762
+ res,
763
+ 200,
764
+ await continueLiveTurn(
765
+ store,
766
+ room.id,
767
+ str(body, "botId"),
768
+ env,
769
+ extras,
770
+ ),
771
+ );
772
+ return;
773
+ }
774
+
613
775
  const channelSteer = path.match(/^\/channels\/([^/]+)\/steer$/);
614
776
  if (channelSteer && method === "POST") {
615
777
  const body = asRecord(await readJson(req));
@@ -859,6 +1021,7 @@ export async function handleRequest(
859
1021
  if (method === "GET" && path === "/settings/models") {
860
1022
  await refreshCopilotCatalog(store.dataDir);
861
1023
  await refreshOpenCodeFreeCatalog(store.dataDir);
1024
+ await refreshReasoningCatalog().catch(() => {});
862
1025
  json(res, 200, publicModels(store.dataDir));
863
1026
  return;
864
1027
  }