Skip to content

Commit 2b54089

Browse files
committed
feat: add Cerebras model provider support and SDK integration
- Add Cerebras to supported providers in README and config - Add Cerebras icon and update icon utilities - Integrate @cerebras/cerebras_cloud_sdk dependency - Implement Cerebras model fetching and completion (streaming & non-streaming) - Expose Cerebras in provider APIs and server logic - Refactor debounce sync utility for dynamic import
1 parent 02cf16a commit 2b54089

File tree

11 files changed

+226
-7
lines changed

11 files changed

+226
-7
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ A clean and simple front-end for large-language models.
2121
- OpenAI
2222
- Gemini
2323
- Anthropic
24+
- Cerebras
2425
- Themes and fun things
2526

2627
## Usage
@@ -44,6 +45,7 @@ To use a remote provider api for llm inference, you can set an environmental var
4445
- `NUXT_OPENAI_API_KEY` for OpenAI
4546
- `NUXT_GEMINI_API_KEY` for Gemini
4647
- `NUXT_ANTHROPIC_API_KEY` for Anthropic
48+
- `NUXT_CEREBRAS_API_KEY` for Cerebras
4749

4850
##### Example
4951

@@ -100,6 +102,7 @@ AUTH_SECRET=
100102
OPENAI_API_KEY=
101103
GEMINI_API_KEY=
102104
ANTHROPIC_API_KEY=
105+
CEREBRAS_API_KEY=
103106
```
104107

105108
4. Run the app

app/assets/icons/cerebras.svg

Lines changed: 1 addition & 0 deletions
Loading

app/utils/icon.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export function getModelProviderIcon(provider?: string) {
77
openai: "simple-icons:openai",
88
anthropic: "simple-icons:anthropic",
99
lmstudio: "local:lmstudio",
10+
cerebras: "local:cerebras",
1011
};
1112
return modelIcons[provider] || "";
1213
}

app/utils/llm/providers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const providers: ProviderMeta[] = [
1010
{ id: "openai", displayName: "OpenAI", icon: "simple-icons:openai" },
1111
{ id: "gemini", displayName: "Gemini", icon: "simple-icons:googlegemini" },
1212
{ id: "anthropic", displayName: "Anthropic", icon: "simple-icons:anthropic" },
13+
{ id: "cerebras", displayName: "Cerebras", icon: "local:cerebras" },
1314
{
1415
id: "lmstudio",
1516
displayName: "LM Studio",

app/utils/sync/debounce.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
1-
import { useSyncStore } from "~/stores/sync";
21
import { debounce } from "../debounce";
32

43
const DEBOUNCE_MS = 500;
54

6-
const _triggerSync = () => {
7-
const sync = useSyncStore();
8-
if (!sync.isSyncing) {
9-
sync.sync();
5+
let _debouncedSync: (() => void) | null = null;
6+
7+
export const triggerDebouncedSync = () => {
8+
// Lazy initialization to avoid store access during module load
9+
if (!_debouncedSync) {
10+
const _triggerSync = () => {
11+
// Import store only when actually needed using dynamic import
12+
import("~/stores/sync").then(({ useSyncStore }) => {
13+
const sync = useSyncStore();
14+
if (!sync.isSyncing) {
15+
sync.sync();
16+
}
17+
});
18+
};
19+
20+
_debouncedSync = debounce(_triggerSync, DEBOUNCE_MS);
1021
}
11-
};
1222

13-
export const triggerDebouncedSync = debounce(_triggerSync, DEBOUNCE_MS);
23+
_debouncedSync();
24+
};

nuxt.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export default defineNuxtConfig({
3636
openaiApiKey: process.env.OPENAI_API_KEY || "",
3737
geminiApiKey: process.env.GEMINI_API_KEY || "",
3838
anthropicApiKey: process.env.ANTHROPIC_API_KEY || "",
39+
cerebrasApiKey: process.env.CEREBRAS_API_KEY || "",
3940
public: {
4041
debug: false,
4142
appVersion: "",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
},
2121
"dependencies": {
2222
"@anthropic-ai/sdk": "^0.40.1",
23+
"@cerebras/cerebras_cloud_sdk": "^1.46.0",
2324
"@google/genai": "^0.12.0",
2425
"@lancedb/lancedb": "^0.19.0",
2526
"@libsql/client": "^0.15.4",

pnpm-lock.yaml

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/api/providers/index.get.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { auth } from "@/utils/auth";
33
import { globalSettings } from "~/utils/db/schema";
44
import { and, eq, type InferSelectModel } from "drizzle-orm";
55
import type { GlobalSettings } from "~/stores/globalSettings";
6+
import { getLMStudioClient } from "~~/server/utils/llm/completionLMStudio";
7+
import { cloudDb } from "~~/server/utils/db/cloud";
68

79
export default defineEventHandler(async (event) => {
810
logger.debug("GET /api/providers");
@@ -35,6 +37,10 @@ export default defineEventHandler(async (event) => {
3537
if (config.anthropicApiKey) {
3638
providers.push("anthropic");
3739
}
40+
// Cerebras
41+
if (config.cerebrasApiKey) {
42+
providers.push("cerebras");
43+
}
3844
// LM Studio
3945
const lmStudio = getLMStudioClient();
4046
const lmStudioVersion = await lmStudio.system.getLMStudioVersion();
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { logger } from "~/utils/logger";
2+
import Cerebras from "@cerebras/cerebras_cloud_sdk";
3+
import type { LocalMessage, Usage, Model } from "~/utils/db/local";
4+
5+
// Client generator
6+
export function getCerebrasClient() {
7+
const config = useRuntimeConfig();
8+
const apiKey = config.cerebrasApiKey;
9+
if (!apiKey) {
10+
throw new Error("Missing CEREBRAS_API_KEY");
11+
}
12+
return new Cerebras({ apiKey });
13+
}
14+
15+
// Fetch models
16+
export async function fetchCerebrasModels() {
17+
const cerebras = getCerebrasClient();
18+
19+
try {
20+
const modelsList = await cerebras.models.list();
21+
const models: Model[] = modelsList.data.map((model) => ({
22+
name: model.id,
23+
displayName:
24+
model.id.charAt(0).toUpperCase() +
25+
model.id.slice(1).replace(/[-.]/g, " "),
26+
provider: "cerebras",
27+
}));
28+
29+
return models;
30+
} catch (error) {
31+
logger.error(error, "GET /api/models/cerebras: Error fetching models");
32+
throw new Error("Failed to fetch models");
33+
}
34+
}
35+
36+
// Non-streaming completion
37+
export async function completionCerebras({
38+
history,
39+
model,
40+
systemPrompt,
41+
}: {
42+
history: LocalMessage[];
43+
model: string;
44+
systemPrompt: string;
45+
}) {
46+
const cerebras = getCerebrasClient();
47+
48+
try {
49+
const formattedMessages = history.map((msg) => ({
50+
role: msg.role as "user" | "assistant",
51+
content: msg.content,
52+
}));
53+
54+
const params = {
55+
messages: [
56+
{ role: "system" as const, content: systemPrompt },
57+
...formattedMessages,
58+
],
59+
model,
60+
};
61+
62+
const completion = await cerebras.chat.completions.create(params);
63+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
64+
const content = (completion as any).choices[0]?.message?.content;
65+
return content;
66+
} catch (error) {
67+
logger.error(error, "Error getting completion from Cerebras");
68+
throw new Error("Internal server error");
69+
}
70+
}
71+
72+
// Streaming completion
73+
export async function streamCerebras({
74+
history,
75+
model,
76+
systemPrompt,
77+
}: {
78+
history: LocalMessage[];
79+
model: string;
80+
systemPrompt: string;
81+
}): Promise<ReadableStream> {
82+
const cerebras = getCerebrasClient();
83+
const encoder = new TextEncoder();
84+
85+
const stream = new ReadableStream({
86+
async start(controller) {
87+
try {
88+
const formattedMessages = [
89+
{ role: "system" as const, content: systemPrompt },
90+
...history.map((msg) => ({
91+
role: msg.role as "user" | "assistant",
92+
content: msg.content,
93+
})),
94+
];
95+
96+
const queryStartTime = performance.now();
97+
let timeToFirstToken = 0;
98+
let responseStartTime = 0;
99+
100+
const completion = await cerebras.chat.completions.create({
101+
model: model,
102+
messages: formattedMessages,
103+
stream: true,
104+
});
105+
106+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
107+
let usageChunk: any;
108+
for await (const chunk of completion) {
109+
if (!timeToFirstToken) {
110+
timeToFirstToken = performance.now() - queryStartTime;
111+
responseStartTime = performance.now();
112+
}
113+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
114+
const text = (chunk as any).choices[0]?.delta?.content || "";
115+
controller.enqueue(
116+
encoder.encode(
117+
`event: messageChunk\ndata: ${JSON.stringify(text)}\n\n`,
118+
),
119+
);
120+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
121+
if ((chunk as any).usage) {
122+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
123+
usageChunk = (chunk as any).usage;
124+
}
125+
}
126+
127+
if (usageChunk) {
128+
const completionTime = performance.now() - responseStartTime;
129+
const usage: Usage | Partial<Usage> = {
130+
promptTokens: usageChunk.prompt_tokens,
131+
completionTokens: usageChunk.completion_tokens,
132+
totalTokens: usageChunk.total_tokens,
133+
completionTime,
134+
tokensPerSecond:
135+
(usageChunk.completion_tokens / completionTime) * 1000,
136+
timeToFirstToken,
137+
temperature: 1,
138+
};
139+
controller.enqueue(
140+
encoder.encode(`event: usage\ndata: ${JSON.stringify(usage)}\n\n`),
141+
);
142+
}
143+
} catch (error) {
144+
logger.error(error, "Error streaming Cerebras");
145+
controller.enqueue(
146+
encoder.encode(
147+
`event: error\ndata: Error streaming Cerebras: ${error}\n\n`,
148+
),
149+
);
150+
} finally {
151+
controller.close();
152+
}
153+
},
154+
});
155+
156+
return stream;
157+
}

0 commit comments

Comments
 (0)