coyotte508 HF Staff commited on
Commit
3b53c7a
·
unverified ·
1 Parent(s): efb37e7

fix CI (#1884)

Browse files

* fix CI

* add playwright as dev dep

* lint

* clean up spaces-config with community tools

* try to make mongo run?

.env CHANGED
@@ -4,7 +4,7 @@
4
  ### Models ###
5
  # Models are sourced exclusively from an OpenAI-compatible base URL.
6
  # Example: https://router.huggingface.co/v1
7
- OPENAI_BASE_URL=
8
 
9
  # Canonical auth token for any OpenAI-compatible provider
10
  OPENAI_API_KEY=#your provider API key (works for HF router, OpenAI, LM Studio, etc.)
 
4
  ### Models ###
5
  # Models are sourced exclusively from an OpenAI-compatible base URL.
6
  # Example: https://router.huggingface.co/v1
7
+ OPENAI_BASE_URL=https://router.huggingface.co/v1
8
 
9
  # Canonical auth token for any OpenAI-compatible provider
10
  OPENAI_API_KEY=#your provider API key (works for HF router, OpenAI, LM Studio, etc.)
.eslintrc.cjs CHANGED
@@ -24,6 +24,7 @@ module.exports = {
24
  extraFileExtensions: [".svelte"],
25
  },
26
  rules: {
 
27
  "require-yield": "off",
28
  "@typescript-eslint/no-explicit-any": "error",
29
  "@typescript-eslint/no-non-null-assertion": "error",
 
24
  extraFileExtensions: [".svelte"],
25
  },
26
  rules: {
27
+ "no-empty": "off",
28
  "require-yield": "off",
29
  "@typescript-eslint/no-explicit-any": "error",
30
  "@typescript-eslint/no-non-null-assertion": "error",
.github/workflows/lint-and-test.yml CHANGED
@@ -39,6 +39,7 @@ jobs:
39
  cache: "npm"
40
  - run: |
41
  npm ci
 
42
  - name: "Tests"
43
  run: |
44
  npm run test
 
39
  cache: "npm"
40
  - run: |
41
  npm ci
42
+ npx playwright install
43
  - name: "Tests"
44
  run: |
45
  npm run test
Dockerfile CHANGED
@@ -21,7 +21,7 @@ RUN touch /app/.env.local
21
 
22
  USER root
23
  RUN apt-get update
24
- RUN apt-get install -y libgomp1
25
 
26
  # ensure npm cache dir exists before adjusting ownership
27
  RUN mkdir -p /home/user/.npm && chown -R 1000:1000 /home/user/.npm
 
21
 
22
  USER root
23
  RUN apt-get update
24
+ RUN apt-get install -y libgomp1 libcurl4
25
 
26
  # ensure npm cache dir exists before adjusting ownership
27
  RUN mkdir -p /home/user/.npm && chown -R 1000:1000 /home/user/.npm
PRIVACY.md CHANGED
@@ -26,7 +26,6 @@ Security and routing facts
26
 
27
  External providers are responsible for their own security and data handling. Please consult each provider’s respective security and privacy policies via the Inference Providers documentation linked above.
28
 
29
-
30
  ## Technical details
31
 
32
  [![chat-ui](https://img.shields.io/github/stars/huggingface/chat-ui)](https://github.com/huggingface/chat-ui)
 
26
 
27
  External providers are responsible for their own security and data handling. Please consult each provider’s respective security and privacy policies via the Inference Providers documentation linked above.
28
 
 
29
  ## Technical details
30
 
31
  [![chat-ui](https://img.shields.io/github/stars/huggingface/chat-ui)](https://github.com/huggingface/chat-ui)
package-lock.json CHANGED
@@ -74,6 +74,7 @@
74
  "js-yaml": "^4.1.0",
75
  "minimist": "^1.2.8",
76
  "mongodb-memory-server": "^10.1.2",
 
77
  "prettier": "^3.5.3",
78
  "prettier-plugin-svelte": "^3.2.6",
79
  "prettier-plugin-tailwindcss": "^0.6.11",
@@ -7968,6 +7969,38 @@
7968
  "dev": true,
7969
  "license": "MIT"
7970
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7971
  "node_modules/postcss": {
7972
  "version": "8.5.4",
7973
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
 
74
  "js-yaml": "^4.1.0",
75
  "minimist": "^1.2.8",
76
  "mongodb-memory-server": "^10.1.2",
77
+ "playwright": "^1.55.1",
78
  "prettier": "^3.5.3",
79
  "prettier-plugin-svelte": "^3.2.6",
80
  "prettier-plugin-tailwindcss": "^0.6.11",
 
7969
  "dev": true,
7970
  "license": "MIT"
7971
  },
7972
+ "node_modules/playwright": {
7973
+ "version": "1.55.1",
7974
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
7975
+ "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
7976
+ "devOptional": true,
7977
+ "license": "Apache-2.0",
7978
+ "dependencies": {
7979
+ "playwright-core": "1.55.1"
7980
+ },
7981
+ "bin": {
7982
+ "playwright": "cli.js"
7983
+ },
7984
+ "engines": {
7985
+ "node": ">=18"
7986
+ },
7987
+ "optionalDependencies": {
7988
+ "fsevents": "2.3.2"
7989
+ }
7990
+ },
7991
+ "node_modules/playwright-core": {
7992
+ "version": "1.55.1",
7993
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
7994
+ "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
7995
+ "devOptional": true,
7996
+ "license": "Apache-2.0",
7997
+ "bin": {
7998
+ "playwright-core": "cli.js"
7999
+ },
8000
+ "engines": {
8001
+ "node": ">=18"
8002
+ }
8003
+ },
8004
  "node_modules/postcss": {
8005
  "version": "8.5.4",
8006
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
package.json CHANGED
@@ -48,6 +48,7 @@
48
  "js-yaml": "^4.1.0",
49
  "minimist": "^1.2.8",
50
  "mongodb-memory-server": "^10.1.2",
 
51
  "prettier": "^3.5.3",
52
  "prettier-plugin-svelte": "^3.2.6",
53
  "prettier-plugin-tailwindcss": "^0.6.11",
 
48
  "js-yaml": "^4.1.0",
49
  "minimist": "^1.2.8",
50
  "mongodb-memory-server": "^10.1.2",
51
+ "playwright": "^1.55.1",
52
  "prettier": "^3.5.3",
53
  "prettier-plugin-svelte": "^3.2.6",
54
  "prettier-plugin-tailwindcss": "^0.6.11",
src/lib/components/NavMenu.svelte CHANGED
@@ -42,7 +42,6 @@
42
 
43
  let {
44
  conversations = $bindable(),
45
- canLogin,
46
  user,
47
  p = $bindable(0),
48
  ondeleteConversation,
 
42
 
43
  let {
44
  conversations = $bindable(),
 
45
  user,
46
  p = $bindable(0),
47
  ondeleteConversation,
src/lib/components/WelcomeModal.svelte CHANGED
@@ -1,6 +1,5 @@
1
  <script lang="ts">
2
  import Modal from "$lib/components/Modal.svelte";
3
- import Logo from "$lib/components/icons/Logo.svelte";
4
  import IconOmni from "$lib/components/icons/IconOmni.svelte";
5
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
6
 
 
1
  <script lang="ts">
2
  import Modal from "$lib/components/Modal.svelte";
 
3
  import IconOmni from "$lib/components/icons/IconOmni.svelte";
4
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
5
 
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -298,7 +298,9 @@
298
 
299
  const blob = await response.blob();
300
  const name = attachment.src.split("/").pop() ?? "attachment";
301
- loadedFiles.push(new File([blob], name, { type: blob.type || "application/octet-stream" }));
 
 
302
  } catch (err) {
303
  console.error("Error loading attachment:", err);
304
  }
 
298
 
299
  const blob = await response.blob();
300
  const name = attachment.src.split("/").pop() ?? "attachment";
301
+ loadedFiles.push(
302
+ new File([blob], name, { type: blob.type || "application/octet-stream" })
303
+ );
304
  } catch (err) {
305
  console.error("Error loading attachment:", err);
306
  }
src/lib/components/chat/MarkdownRenderer.svelte CHANGED
@@ -86,10 +86,6 @@
86
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
87
  {@html token.html}
88
  {:else if token.type === "code"}
89
- <CodeBlock
90
- code={token.code}
91
- rawCode={token.rawCode}
92
- loading={loading && !token.isClosed}
93
- />
94
  {/if}
95
  {/each}
 
86
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
87
  {@html token.html}
88
  {:else if token.type === "code"}
89
+ <CodeBlock code={token.code} rawCode={token.rawCode} loading={loading && !token.isClosed} />
 
 
 
 
90
  {/if}
91
  {/each}
src/lib/migrations/routines/06-trim-message-updates.ts CHANGED
@@ -12,7 +12,11 @@ import { logger } from "$lib/server/logger";
12
  function convertMessageUpdate(message: Message, update: unknown): MessageUpdate | null {
13
  try {
14
  // Trim legacy web search updates entirely
15
- if (typeof update === "object" && update !== null && (update as any).type === "webSearch") {
 
 
 
 
16
  return null;
17
  }
18
 
 
12
  function convertMessageUpdate(message: Message, update: unknown): MessageUpdate | null {
13
  try {
14
  // Trim legacy web search updates entirely
15
+ if (
16
+ typeof update === "object" &&
17
+ update !== null &&
18
+ (update as { type: string }).type === "webSearch"
19
+ ) {
20
  return null;
21
  }
22
 
src/lib/server/api/routes/groups/misc.ts CHANGED
@@ -63,36 +63,6 @@ export const misc = new Elysia()
63
  isAdmin: locals.isAdmin,
64
  } satisfies FeatureFlags;
65
  })
66
- .get("/spaces-config", async ({ query }) => {
67
- if (config.COMMUNITY_TOOLS !== "true") {
68
- throw new Error("Community tools are not enabled");
69
- }
70
-
71
- const space = query.space;
72
-
73
- if (!space) {
74
- throw new Error("Missing space");
75
- }
76
-
77
- // Extract namespace from space URL or use as-is if it's already in namespace format
78
- let namespace = null;
79
- if (space.startsWith("https://huggingface.co/spaces/")) {
80
- namespace = space.split("/").slice(-2).join("/");
81
- } else if (space.match(/^[^/]+\/[^/]+$/)) {
82
- namespace = space;
83
- }
84
-
85
- if (!namespace) {
86
- throw new Error("Invalid space name. Specify a namespace or a full URL on huggingface.co.");
87
- }
88
-
89
- try {
90
- const api = await (await Client.connect(namespace)).view_api();
91
- return api as ApiReturnType;
92
- } catch (e) {
93
- throw new Error("Error fetching space API. Is the name correct?");
94
- }
95
- })
96
  .get("/export", async ({ locals }) => {
97
  if (!locals.user) {
98
  throw new Error("Not logged in");
 
63
  isAdmin: locals.isAdmin,
64
  } satisfies FeatureFlags;
65
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  .get("/export", async ({ locals }) => {
67
  if (!locals.user) {
68
  throw new Error("Not logged in");
src/lib/server/endpoints/openai/endpointOai.ts CHANGED
@@ -123,7 +123,7 @@ export async function endpointOai(
123
  stop: parameters?.stop,
124
  temperature: parameters?.temperature,
125
  top_p: parameters?.top_p,
126
- frequency_penalty: parameters?.frequency_penalty,
127
  presence_penalty: parameters?.presence_penalty,
128
  };
129
 
@@ -173,7 +173,7 @@ export async function endpointOai(
173
  stop: parameters?.stop,
174
  temperature: parameters?.temperature,
175
  top_p: parameters?.top_p,
176
- frequency_penalty: parameters?.frequency_penalty,
177
  presence_penalty: parameters?.presence_penalty,
178
  };
179
 
 
123
  stop: parameters?.stop,
124
  temperature: parameters?.temperature,
125
  top_p: parameters?.top_p,
126
+ frequency_penalty: parameters?.frequency_penalty,
127
  presence_penalty: parameters?.presence_penalty,
128
  };
129
 
 
173
  stop: parameters?.stop,
174
  temperature: parameters?.temperature,
175
  top_p: parameters?.top_p,
176
+ frequency_penalty: parameters?.frequency_penalty,
177
  presence_penalty: parameters?.presence_penalty,
178
  };
179
 
src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts CHANGED
@@ -16,9 +16,10 @@ export async function* openAIChatToTextGenerationStream(
16
  let thinkOpen = false;
17
 
18
  for await (const completion of completionStream) {
 
19
  // Check if this chunk contains router metadata (first chunk from llm-router)
20
- if (!metadataYielded && (completion as any)["x-router-metadata"]) {
21
- const metadata = (completion as any)["x-router-metadata"];
22
  yield {
23
  token: {
24
  id: tokenId++,
@@ -44,8 +45,11 @@ export async function* openAIChatToTextGenerationStream(
44
  }
45
  }
46
  const { choices } = completion;
47
- const delta: any = choices?.[0]?.delta ?? {};
48
- const content: string = (delta?.content as string) ?? "";
 
 
 
49
  const reasoning: string =
50
  typeof delta?.reasoning === "string"
51
  ? (delta.reasoning as string)
@@ -158,7 +162,10 @@ export async function* openAIChatToTextGenerationSingle(
158
  completion: OpenAI.Chat.Completions.ChatCompletion,
159
  getRouterMetadata?: () => { route?: string; model?: string }
160
  ) {
161
- const message: any = completion.choices?.[0]?.message ?? {};
 
 
 
162
  let content: string = message?.content || "";
163
  // Provider-dependent reasoning shapes (non-streaming)
164
  const r: string =
 
16
  let thinkOpen = false;
17
 
18
  for await (const completion of completionStream) {
19
+ const retyped = completion as { "x-router-metadata"?: { route: string; model: string } };
20
  // Check if this chunk contains router metadata (first chunk from llm-router)
21
+ if (!metadataYielded && retyped["x-router-metadata"]) {
22
+ const metadata = retyped["x-router-metadata"];
23
  yield {
24
  token: {
25
  id: tokenId++,
 
45
  }
46
  }
47
  const { choices } = completion;
48
+ const delta: OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta & {
49
+ reasoning?: string;
50
+ reasoning_content?: string;
51
+ } = choices?.[0]?.delta ?? {};
52
+ const content: string = delta.content ?? "";
53
  const reasoning: string =
54
  typeof delta?.reasoning === "string"
55
  ? (delta.reasoning as string)
 
162
  completion: OpenAI.Chat.Completions.ChatCompletion,
163
  getRouterMetadata?: () => { route?: string; model?: string }
164
  ) {
165
+ const message: NonNullable<OpenAI.Chat.Completions.ChatCompletion.Choice>["message"] & {
166
+ reasoning?: string;
167
+ reasoning_content?: string;
168
+ } = completion.choices?.[0]?.message ?? {};
169
  let content: string = message?.content || "";
170
  // Provider-dependent reasoning shapes (non-streaming)
171
  const r: string =
src/lib/server/models.ts CHANGED
@@ -64,7 +64,7 @@ const modelConfig = z.object({
64
  stop: z.array(z.string()).optional(),
65
  top_p: z.number().positive().optional(),
66
  top_k: z.number().positive().optional(),
67
- frequency_penalty: z.number().min(-2).max(2).optional(),
68
  presence_penalty: z.number().min(-2).max(2).optional(),
69
  })
70
  .passthrough()
@@ -220,6 +220,8 @@ if (modelOverrides.length) {
220
  if (!override) return model;
221
 
222
  const { id, name, ...rest } = override;
 
 
223
 
224
  return {
225
  ...model,
@@ -291,7 +293,7 @@ const routerAliasId = (config.PUBLIC_LLM_ROUTER_ALIAS_ID || "omni").trim() || "o
291
  const routerMultimodalEnabled =
292
  (config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
293
 
294
- let decorated = builtModels as any[];
295
 
296
  if (archBase) {
297
  // Build a minimal model config for the alias
@@ -304,12 +306,12 @@ if (archBase) {
304
  endpoints: [
305
  {
306
  type: "openai" as const,
307
- baseURL: openaiBaseUrl!,
308
  },
309
  ],
310
  // Keep the alias visible
311
  unlisted: false,
312
- } as any;
313
 
314
  if (routerMultimodalEnabled) {
315
  aliasRaw.multimodal = true;
@@ -318,13 +320,12 @@ if (archBase) {
318
 
319
  const aliasBase = await processModel(aliasRaw);
320
  // Create a self-referential ProcessedModel for the router endpoint
321
- let aliasModel: any = {};
322
- aliasModel = {
323
  ...aliasBase,
324
  isRouter: true,
325
  // getEndpoint uses the router wrapper regardless of the endpoints array
326
  getEndpoint: async (): Promise<Endpoint> => makeRouterEndpoint(aliasModel),
327
- };
328
 
329
  // Put alias first
330
  decorated = [aliasModel, ...decorated];
 
64
  stop: z.array(z.string()).optional(),
65
  top_p: z.number().positive().optional(),
66
  top_k: z.number().positive().optional(),
67
+ frequency_penalty: z.number().min(-2).max(2).optional(),
68
  presence_penalty: z.number().min(-2).max(2).optional(),
69
  })
70
  .passthrough()
 
220
  if (!override) return model;
221
 
222
  const { id, name, ...rest } = override;
223
+ void id;
224
+ void name;
225
 
226
  return {
227
  ...model,
 
293
  const routerMultimodalEnabled =
294
  (config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
295
 
296
+ let decorated = builtModels as ProcessedModel[];
297
 
298
  if (archBase) {
299
  // Build a minimal model config for the alias
 
306
  endpoints: [
307
  {
308
  type: "openai" as const,
309
+ baseURL: openaiBaseUrl,
310
  },
311
  ],
312
  // Keep the alias visible
313
  unlisted: false,
314
+ } as ProcessedModel;
315
 
316
  if (routerMultimodalEnabled) {
317
  aliasRaw.multimodal = true;
 
320
 
321
  const aliasBase = await processModel(aliasRaw);
322
  // Create a self-referential ProcessedModel for the router endpoint
323
+ const aliasModel: ProcessedModel = {
 
324
  ...aliasBase,
325
  isRouter: true,
326
  // getEndpoint uses the router wrapper regardless of the endpoints array
327
  getEndpoint: async (): Promise<Endpoint> => makeRouterEndpoint(aliasModel),
328
+ } as ProcessedModel;
329
 
330
  // Put alias first
331
  decorated = [aliasModel, ...decorated];
src/lib/server/router/arch.ts CHANGED
@@ -106,7 +106,7 @@ export async function archSelectRoute(
106
  });
107
  clearTimeout(to);
108
  if (!resp.ok) throw new Error(`arch-router ${resp.status}`);
109
- const data: any = await resp.json();
110
  const text = (data?.choices?.[0]?.message?.content ?? "").toString().trim();
111
  const raw = parseRouteName(text);
112
 
@@ -114,7 +114,7 @@ export async function archSelectRoute(
114
  const chosen = raw === "other" ? other : raw || "casual_conversation";
115
  const exists = routes.some((r) => r.name === chosen);
116
  return { routeName: exists ? chosen : "casual_conversation" };
117
- } catch (e: any) {
118
  clearTimeout(to);
119
  logger.warn({ err: String(e), traceId }, "arch router selection failed");
120
  return { routeName: "arch_router_failure" };
 
106
  });
107
  clearTimeout(to);
108
  if (!resp.ok) throw new Error(`arch-router ${resp.status}`);
109
+ const data: { choices: { message: { content: string } }[] } = await resp.json();
110
  const text = (data?.choices?.[0]?.message?.content ?? "").toString().trim();
111
  const raw = parseRouteName(text);
112
 
 
114
  const chosen = raw === "other" ? other : raw || "casual_conversation";
115
  const exists = routes.some((r) => r.name === chosen);
116
  return { routeName: exists ? chosen : "casual_conversation" };
117
+ } catch (e) {
118
  clearTimeout(to);
119
  logger.warn({ err: String(e), traceId }, "arch router selection failed");
120
  return { routeName: "arch_router_failure" };
src/lib/server/router/endpoint.ts CHANGED
@@ -1,4 +1,9 @@
1
- import type { Endpoint, EndpointParameters, EndpointMessage } from "../endpoints/endpoints";
 
 
 
 
 
2
  import endpoints from "../endpoints/endpoints";
3
  import type { ProcessedModel } from "../models";
4
  import { config } from "$lib/server/config";
@@ -17,6 +22,7 @@ function stripReasoningBlocks(text: string): string {
17
 
18
  function stripReasoningFromMessage(message: EndpointMessage): EndpointMessage {
19
  const { reasoning: _reasoning, ...rest } = message;
 
20
  const content =
21
  typeof message.content === "string" ? stripReasoningBlocks(message.content) : message.content;
22
  return {
@@ -47,7 +53,7 @@ export async function makeRouterEndpoint(routerModel: ProcessedModel): Promise<E
47
  let modelForCall: ProcessedModel | undefined;
48
  try {
49
  const mod = await import("../models");
50
- const all = (mod as any).models as ProcessedModel[];
51
  modelForCall = all?.find((m) => m.id === candidateModelId || m.name === candidateModelId);
52
  } catch (e) {
53
  logger.warn({ err: String(e) }, "[router] failed to load models for candidate lookup");
@@ -75,7 +81,7 @@ export async function makeRouterEndpoint(routerModel: ProcessedModel): Promise<E
75
 
76
  // Yield router metadata for immediate UI display, using the actual candidate
77
  async function* metadataThenStream(
78
- gen: AsyncGenerator<any>,
79
  actualModel: string,
80
  selectedRoute: string
81
  ) {
@@ -84,14 +90,14 @@ export async function makeRouterEndpoint(routerModel: ProcessedModel): Promise<E
84
  generated_text: null,
85
  details: null,
86
  routerMetadata: { route: selectedRoute, model: actualModel },
87
- } as any;
88
  for await (const ev of gen) yield ev;
89
  }
90
 
91
  async function findFirstMultimodalCandidateId(): Promise<string | undefined> {
92
  try {
93
  const mod = await import("../models");
94
- const all = (mod as any).models as ProcessedModel[];
95
  const first = all?.find((m) => !m.isRouter && m.multimodal);
96
  return first?.id ?? first?.name;
97
  } catch (e) {
@@ -132,7 +138,7 @@ export async function makeRouterEndpoint(routerModel: ProcessedModel): Promise<E
132
  const fallbackModel = config.LLM_ROUTER_FALLBACK_MODEL || routerModel.id;
133
  const { candidates } = resolveRouteModels(routeName, routes, fallbackModel);
134
 
135
- let lastErr: any = undefined;
136
  for (const candidate of candidates) {
137
  try {
138
  logger.info({ route: routeName, model: candidate }, "[router] trying candidate");
 
1
+ import type {
2
+ Endpoint,
3
+ EndpointParameters,
4
+ EndpointMessage,
5
+ TextGenerationStreamOutputSimplified,
6
+ } from "../endpoints/endpoints";
7
  import endpoints from "../endpoints/endpoints";
8
  import type { ProcessedModel } from "../models";
9
  import { config } from "$lib/server/config";
 
22
 
23
  function stripReasoningFromMessage(message: EndpointMessage): EndpointMessage {
24
  const { reasoning: _reasoning, ...rest } = message;
25
+ void _reasoning;
26
  const content =
27
  typeof message.content === "string" ? stripReasoningBlocks(message.content) : message.content;
28
  return {
 
53
  let modelForCall: ProcessedModel | undefined;
54
  try {
55
  const mod = await import("../models");
56
+ const all = (mod as { models: ProcessedModel[] }).models;
57
  modelForCall = all?.find((m) => m.id === candidateModelId || m.name === candidateModelId);
58
  } catch (e) {
59
  logger.warn({ err: String(e) }, "[router] failed to load models for candidate lookup");
 
81
 
82
  // Yield router metadata for immediate UI display, using the actual candidate
83
  async function* metadataThenStream(
84
+ gen: AsyncGenerator<TextGenerationStreamOutputSimplified>,
85
  actualModel: string,
86
  selectedRoute: string
87
  ) {
 
90
  generated_text: null,
91
  details: null,
92
  routerMetadata: { route: selectedRoute, model: actualModel },
93
+ };
94
  for await (const ev of gen) yield ev;
95
  }
96
 
97
  async function findFirstMultimodalCandidateId(): Promise<string | undefined> {
98
  try {
99
  const mod = await import("../models");
100
+ const all = (mod as { models: ProcessedModel[] }).models;
101
  const first = all?.find((m) => !m.isRouter && m.multimodal);
102
  return first?.id ?? first?.name;
103
  } catch (e) {
 
138
  const fallbackModel = config.LLM_ROUTER_FALLBACK_MODEL || routerModel.id;
139
  const { candidates } = resolveRouteModels(routeName, routes, fallbackModel);
140
 
141
+ let lastErr: unknown = undefined;
142
  for (const candidate of candidates) {
143
  try {
144
  logger.info({ route: routeName, model: candidate }, "[router] trying candidate");
src/lib/server/textGeneration/reasoning.ts CHANGED
@@ -21,9 +21,9 @@ export async function generateSummaryOfReasoning(
21
  preprompt: `You are tasked with summarizing the latest reasoning steps. Never describe results of the reasoning, only the process. Remain vague in your summary.
22
  The text might be incomplete, try your best to summarize it in one very short sentence, starting with a gerund and ending with three points.
23
  Example: "Thinking about life...", "Summarizing the results...", "Processing the input..."`,
24
- generateSettings: {
25
- max_tokens: 50,
26
- },
27
  modelId,
28
  })
29
  );
 
21
  preprompt: `You are tasked with summarizing the latest reasoning steps. Never describe results of the reasoning, only the process. Remain vague in your summary.
22
  The text might be incomplete, try your best to summarize it in one very short sentence, starting with a gerund and ending with three points.
23
  Example: "Thinking about life...", "Summarizing the results...", "Processing the input..."`,
24
+ generateSettings: {
25
+ max_tokens: 50,
26
+ },
27
  modelId,
28
  })
29
  );
src/lib/server/textGeneration/title.ts CHANGED
@@ -44,9 +44,9 @@ Do not answer the question.
44
  Do not include the word prompt into your response.
45
  Do not include quotes, emojis, hashtags or trailing punctuation.
46
  Return ONLY the title text.`,
47
- generateSettings: {
48
- max_tokens: 30,
49
- },
50
  modelId,
51
  })
52
  )
 
44
  Do not include the word prompt into your response.
45
  Do not include quotes, emojis, hashtags or trailing punctuation.
46
  Return ONLY the title text.`,
47
+ generateSettings: {
48
+ max_tokens: 30,
49
+ },
50
  modelId,
51
  })
52
  )
src/lib/utils/marked.ts CHANGED
@@ -174,6 +174,7 @@ function createMarkedInstance(sources: SimpleSource[]): Marked {
174
  }
175
  function isFencedBlockClosed(raw?: string): boolean {
176
  if (!raw) return true;
 
177
  const trimmed = raw.replace(/[\s\u0000]+$/, "");
178
  const openingFenceMatch = trimmed.match(/^([`~]{3,})/);
179
  if (!openingFenceMatch) {
@@ -203,14 +204,14 @@ export async function processTokens(content: string, sources: SimpleSource[]): P
203
 
204
  const processedTokens = await Promise.all(
205
  tokens.map(async (token) => {
206
- if (token.type === "code") {
207
- return {
208
- type: "code" as const,
209
- lang: token.lang,
210
- code: hljs.highlightAuto(token.text, hljs.getLanguage(token.lang)?.aliases).value,
211
- rawCode: token.text,
212
- isClosed: isFencedBlockClosed(token.raw ?? ""),
213
- };
214
  } else {
215
  return {
216
  type: "text" as const,
@@ -227,14 +228,14 @@ export function processTokensSync(content: string, sources: SimpleSource[]): Tok
227
  const marked = createMarkedInstance(sources);
228
  const tokens = marked.lexer(content);
229
  return tokens.map((token) => {
230
- if (token.type === "code") {
231
- return {
232
- type: "code" as const,
233
- lang: token.lang,
234
- code: hljs.highlightAuto(token.text, hljs.getLanguage(token.lang)?.aliases).value,
235
- rawCode: token.text,
236
- isClosed: isFencedBlockClosed(token.raw ?? ""),
237
- };
238
  }
239
  return { type: "text" as const, html: marked.parse(token.raw) };
240
  });
 
174
  }
175
  function isFencedBlockClosed(raw?: string): boolean {
176
  if (!raw) return true;
177
+ /* eslint-disable-next-line no-control-regex */
178
  const trimmed = raw.replace(/[\s\u0000]+$/, "");
179
  const openingFenceMatch = trimmed.match(/^([`~]{3,})/);
180
  if (!openingFenceMatch) {
 
204
 
205
  const processedTokens = await Promise.all(
206
  tokens.map(async (token) => {
207
+ if (token.type === "code") {
208
+ return {
209
+ type: "code" as const,
210
+ lang: token.lang,
211
+ code: hljs.highlightAuto(token.text, hljs.getLanguage(token.lang)?.aliases).value,
212
+ rawCode: token.text,
213
+ isClosed: isFencedBlockClosed(token.raw ?? ""),
214
+ };
215
  } else {
216
  return {
217
  type: "text" as const,
 
228
  const marked = createMarkedInstance(sources);
229
  const tokens = marked.lexer(content);
230
  return tokens.map((token) => {
231
+ if (token.type === "code") {
232
+ return {
233
+ type: "code" as const,
234
+ lang: token.lang,
235
+ code: hljs.highlightAuto(token.text, hljs.getLanguage(token.lang)?.aliases).value,
236
+ rawCode: token.text,
237
+ isClosed: isFencedBlockClosed(token.raw ?? ""),
238
+ };
239
  }
240
  return { type: "text" as const, html: marked.parse(token.raw) };
241
  });
src/styles/highlight-js.css CHANGED
@@ -20,23 +20,23 @@ hue-6-2: #c18401
20
  */
21
 
22
  .hljs {
23
- display: block;
24
- overflow-x: auto;
25
- padding: 0.5em;
26
- color: #383a42;
27
- background: #fafafa;
28
  }
29
 
30
  .hljs-comment,
31
  .hljs-quote {
32
- color: #a0a1a7;
33
- font-style: italic;
34
  }
35
 
36
  .hljs-doctag,
37
  .hljs-keyword,
38
  .hljs-formula {
39
- color: #a626a4;
40
  }
41
 
42
  .hljs-section,
@@ -44,11 +44,11 @@ hue-6-2: #c18401
44
  .hljs-selector-tag,
45
  .hljs-deletion,
46
  .hljs-subst {
47
- color: #e45649;
48
  }
49
 
50
  .hljs-literal {
51
- color: #0184bb;
52
  }
53
 
54
  .hljs-string,
@@ -56,12 +56,12 @@ hue-6-2: #c18401
56
  .hljs-addition,
57
  .hljs-attribute,
58
  .hljs-meta-string {
59
- color: #50a14f;
60
  }
61
 
62
  .hljs-built_in,
63
  .hljs-class .hljs-title {
64
- color: #c18401;
65
  }
66
 
67
  .hljs-attr,
@@ -72,7 +72,7 @@ hue-6-2: #c18401
72
  .hljs-selector-attr,
73
  .hljs-selector-pseudo,
74
  .hljs-number {
75
- color: #986801;
76
  }
77
 
78
  .hljs-symbol,
@@ -81,19 +81,19 @@ hue-6-2: #c18401
81
  .hljs-meta,
82
  .hljs-selector-id,
83
  .hljs-title {
84
- color: #4078f2;
85
  }
86
 
87
  .hljs-emphasis {
88
- font-style: italic;
89
  }
90
 
91
  .hljs-strong {
92
- font-weight: bold;
93
  }
94
 
95
  .hljs-link {
96
- text-decoration: underline;
97
  }
98
 
99
  /* Atom One Dark (v9.16.2) scoped to .dark */
@@ -118,23 +118,23 @@ hue-6-2: #e6c07b
118
  */
119
 
120
  .dark .hljs {
121
- display: block;
122
- overflow-x: auto;
123
- padding: 0.5em;
124
- color: #abb2bf;
125
- background: #282c34;
126
  }
127
 
128
  .dark .hljs-comment,
129
  .dark .hljs-quote {
130
- color: #5c6370;
131
- font-style: italic;
132
  }
133
 
134
  .dark .hljs-doctag,
135
  .dark .hljs-keyword,
136
  .dark .hljs-formula {
137
- color: #c678dd;
138
  }
139
 
140
  .dark .hljs-section,
@@ -142,11 +142,11 @@ hue-6-2: #e6c07b
142
  .dark .hljs-selector-tag,
143
  .dark .hljs-deletion,
144
  .dark .hljs-subst {
145
- color: #e06c75;
146
  }
147
 
148
  .dark .hljs-literal {
149
- color: #56b6c2;
150
  }
151
 
152
  .dark .hljs-string,
@@ -154,12 +154,12 @@ hue-6-2: #e6c07b
154
  .dark .hljs-addition,
155
  .dark .hljs-attribute,
156
  .dark .hljs-meta-string {
157
- color: #98c379;
158
  }
159
 
160
  .dark .hljs-built_in,
161
  .dark .hljs-class .hljs-title {
162
- color: #e6c07b;
163
  }
164
 
165
  .dark .hljs-attr,
@@ -170,7 +170,7 @@ hue-6-2: #e6c07b
170
  .dark .hljs-selector-attr,
171
  .dark .hljs-selector-pseudo,
172
  .dark .hljs-number {
173
- color: #d19a66;
174
  }
175
 
176
  .dark .hljs-symbol,
@@ -179,17 +179,17 @@ hue-6-2: #e6c07b
179
  .dark .hljs-meta,
180
  .dark .hljs-selector-id,
181
  .dark .hljs-title {
182
- color: #61aeee;
183
  }
184
 
185
  .dark .hljs-emphasis {
186
- font-style: italic;
187
  }
188
 
189
  .dark .hljs-strong {
190
- font-weight: bold;
191
  }
192
 
193
  .dark .hljs-link {
194
- text-decoration: underline;
195
  }
 
20
  */
21
 
22
  .hljs {
23
+ display: block;
24
+ overflow-x: auto;
25
+ padding: 0.5em;
26
+ color: #383a42;
27
+ background: #fafafa;
28
  }
29
 
30
  .hljs-comment,
31
  .hljs-quote {
32
+ color: #a0a1a7;
33
+ font-style: italic;
34
  }
35
 
36
  .hljs-doctag,
37
  .hljs-keyword,
38
  .hljs-formula {
39
+ color: #a626a4;
40
  }
41
 
42
  .hljs-section,
 
44
  .hljs-selector-tag,
45
  .hljs-deletion,
46
  .hljs-subst {
47
+ color: #e45649;
48
  }
49
 
50
  .hljs-literal {
51
+ color: #0184bb;
52
  }
53
 
54
  .hljs-string,
 
56
  .hljs-addition,
57
  .hljs-attribute,
58
  .hljs-meta-string {
59
+ color: #50a14f;
60
  }
61
 
62
  .hljs-built_in,
63
  .hljs-class .hljs-title {
64
+ color: #c18401;
65
  }
66
 
67
  .hljs-attr,
 
72
  .hljs-selector-attr,
73
  .hljs-selector-pseudo,
74
  .hljs-number {
75
+ color: #986801;
76
  }
77
 
78
  .hljs-symbol,
 
81
  .hljs-meta,
82
  .hljs-selector-id,
83
  .hljs-title {
84
+ color: #4078f2;
85
  }
86
 
87
  .hljs-emphasis {
88
+ font-style: italic;
89
  }
90
 
91
  .hljs-strong {
92
+ font-weight: bold;
93
  }
94
 
95
  .hljs-link {
96
+ text-decoration: underline;
97
  }
98
 
99
  /* Atom One Dark (v9.16.2) scoped to .dark */
 
118
  */
119
 
120
  .dark .hljs {
121
+ display: block;
122
+ overflow-x: auto;
123
+ padding: 0.5em;
124
+ color: #abb2bf;
125
+ background: #282c34;
126
  }
127
 
128
  .dark .hljs-comment,
129
  .dark .hljs-quote {
130
+ color: #5c6370;
131
+ font-style: italic;
132
  }
133
 
134
  .dark .hljs-doctag,
135
  .dark .hljs-keyword,
136
  .dark .hljs-formula {
137
+ color: #c678dd;
138
  }
139
 
140
  .dark .hljs-section,
 
142
  .dark .hljs-selector-tag,
143
  .dark .hljs-deletion,
144
  .dark .hljs-subst {
145
+ color: #e06c75;
146
  }
147
 
148
  .dark .hljs-literal {
149
+ color: #56b6c2;
150
  }
151
 
152
  .dark .hljs-string,
 
154
  .dark .hljs-addition,
155
  .dark .hljs-attribute,
156
  .dark .hljs-meta-string {
157
+ color: #98c379;
158
  }
159
 
160
  .dark .hljs-built_in,
161
  .dark .hljs-class .hljs-title {
162
+ color: #e6c07b;
163
  }
164
 
165
  .dark .hljs-attr,
 
170
  .dark .hljs-selector-attr,
171
  .dark .hljs-selector-pseudo,
172
  .dark .hljs-number {
173
+ color: #d19a66;
174
  }
175
 
176
  .dark .hljs-symbol,
 
179
  .dark .hljs-meta,
180
  .dark .hljs-selector-id,
181
  .dark .hljs-title {
182
+ color: #61aeee;
183
  }
184
 
185
  .dark .hljs-emphasis {
186
+ font-style: italic;
187
  }
188
 
189
  .dark .hljs-strong {
190
+ font-weight: bold;
191
  }
192
 
193
  .dark .hljs-link {
194
+ text-decoration: underline;
195
  }