Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions frontend/src/components/LLMSelection/ChutesAiOptions/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useState, useEffect } from "react";
import System from "@/models/system";

export default function ChutesAiOptions({ settings }) {
const [inputValue, setInputValue] = useState(settings?.ChutesApiKey);
const [apiKey, setApiKey] = useState(settings?.ChutesApiKey);

return (
<div className="flex gap-[36px] mt-1.5">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Chutes API Key
</label>
<input
type="password"
name="ChutesApiKey"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="Chutes API Key"
defaultValue={settings?.ChutesApiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
onChange={(e) => setInputValue(e.target.value)}
onBlur={() => setApiKey(inputValue)}
/>
</div>

{!settings?.credentialsOnly && (
<ChutesAIModelSelection settings={settings} apiKey={apiKey} />
)}
</div>
);
}

function ChutesAIModelSelection({ apiKey, settings }) {
const [customModels, setCustomModels] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function findCustomModels() {
if (!apiKey) {
setCustomModels([]);
setLoading(true);
return;
}

try {
setLoading(true);
const { models } = await System.customModels("chutes", apiKey);
setCustomModels(models || []);
} catch (error) {
console.error("Failed to fetch custom models:", error);
setCustomModels([]);
} finally {
setLoading(false);
}
}
findCustomModels();
}, [apiKey]);

if (loading) {
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Chat Model Selection
</label>
<select
name="ChutesModelPref"
disabled={true}
className="border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
--loading available models--
</option>
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Enter a valid API key to view all available models for your account.
</p>
</div>
);
}

return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Chat Model Selection
</label>
<select
name="ChutesModelPref"
required={true}
className="border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{customModels.length > 0 && (
<optgroup label="Available models">
{customModels.map((model) => {
return (
<option
key={model.id}
value={model.id}
selected={settings?.ChutesModelPref === model.id}
>
{model.id}
</option>
);
})}
</optgroup>
)}
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Select the Chutes AI model you want to use for your conversations.
</p>
</div>
);
}
6 changes: 6 additions & 0 deletions frontend/src/components/ProviderPrivacy/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import DockerModelRunnerLogo from "@/media/llmprovider/docker-model-runner.png";
import PrivateModeLogo from "@/media/llmprovider/privatemode.png";
import SambaNovaLogo from "@/media/llmprovider/sambanova.png";
import LemonadeLogo from "@/media/llmprovider/lemonade.png";
import ChutesLogo from "@/media/llmprovider/chutes.png";

const LLM_PROVIDER_PRIVACY_MAP = {
openai: {
Expand Down Expand Up @@ -252,6 +253,11 @@ const LLM_PROVIDER_PRIVACY_MAP = {
],
logo: LemonadeLogo,
},
chutes: {
name: "Chutes AI",
policyUrl: "https://chutes.ai/privacy",
logo: ChutesLogo,
},
};

const VECTOR_DB_PROVIDER_PRIVACY_MAP = {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/useGetProvidersModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const PROVIDER_DEFAULT_MODELS = {
"generic-openai": [],
bedrock: [],
xai: ["grok-beta"],
chutes: [],
};

// For providers with large model lists (e.g. togetherAi) - we subgroup the options
Expand Down
Binary file added frontend/src/media/llmprovider/chutes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions frontend/src/pages/GeneralSettings/LLMPreference/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import DockerModelRunnerLogo from "@/media/llmprovider/docker-model-runner.png";
import PrivateModeLogo from "@/media/llmprovider/privatemode.png";
import SambaNovaLogo from "@/media/llmprovider/sambanova.png";
import LemonadeLogo from "@/media/llmprovider/lemonade.png";
import ChutesLogo from "@/media/llmprovider/chutes.png";

import PreLoader from "@/components/Preloader";
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
Expand Down Expand Up @@ -79,6 +80,7 @@ import DockerModelRunnerOptions from "@/components/LLMSelection/DockerModelRunne
import PrivateModeOptions from "@/components/LLMSelection/PrivateModeOptions";
import SambaNovaOptions from "@/components/LLMSelection/SambaNovaOptions";
import LemonadeOptions from "@/components/LLMSelection/LemonadeOptions";
import ChutesAiOptions from "@/components/LLMSelection/ChutesAiOptions";

import LLMItem from "@/components/LLMSelection/LLMItem";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
Expand Down Expand Up @@ -401,6 +403,15 @@ export const AVAILABLE_LLM_PROVIDERS = [
description: "Run GiteeAI's powerful LLMs.",
requiredConfig: ["GiteeAIApiKey"],
},
{
name: "Chutes AI",
value: "chutes",
logo: ChutesLogo,
options: (settings) => <ChutesAiOptions settings={settings} />,
description:
"Decentralized AI inference on Bittensor SN64, TEE-secured, OpenAI-compatible.",
requiredConfig: ["ChutesApiKey"],
},
{
name: "Generic OpenAI",
value: "generic-openai",
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import DockerModelRunnerLogo from "@/media/llmprovider/docker-model-runner.png";
import PrivateModeLogo from "@/media/llmprovider/privatemode.png";
import SambaNovaLogo from "@/media/llmprovider/sambanova.png";
import LemonadeLogo from "@/media/llmprovider/lemonade.png";
import ChutesLogo from "@/media/llmprovider/chutes.png";

import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions";
Expand Down Expand Up @@ -71,6 +72,7 @@ import DockerModelRunnerOptions from "@/components/LLMSelection/DockerModelRunne
import PrivateModeOptions from "@/components/LLMSelection/PrivateModeOptions";
import SambaNovaOptions from "@/components/LLMSelection/SambaNovaOptions";
import LemonadeOptions from "@/components/LLMSelection/LemonadeOptions";
import ChutesAiOptions from "@/components/LLMSelection/ChutesAiOptions";

import LLMItem from "@/components/LLMSelection/LLMItem";
import System from "@/models/system";
Expand Down Expand Up @@ -336,6 +338,14 @@ const LLMS = [
options: (settings) => <GiteeAiOptions settings={settings} />,
description: "Run GiteeAI's powerful LLMs.",
},
{
name: "Chutes AI",
value: "chutes",
logo: ChutesLogo,
options: (settings) => <ChutesAiOptions settings={settings} />,
description:
"Decentralized AI inference on Bittensor SN64, TEE-secured, OpenAI-compatible.",
},
];

export default function LLMPreference({
Expand Down
186 changes: 186 additions & 0 deletions server/utils/AiProviders/chutes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const {
LLMPerformanceMonitor,
} = require("../../helpers/chat/LLMPerformanceMonitor");
const {
handleDefaultStreamResponseV2,
} = require("../../helpers/chat/responses");
const { MODEL_MAP } = require("../modelMap");

class ChutesLLM {
constructor(embedder = null, modelPreference = null) {
const { OpenAI: OpenAIApi } = require("openai");
if (!process.env.CHUTES_API_KEY)
throw new Error("No Chutes API key was set.");

this.className = "ChutesLLM";
this.openai = new OpenAIApi({
baseURL: "https://llm.chutes.ai/v1",
apiKey: process.env.CHUTES_API_KEY,
});
this.model =
modelPreference || process.env.CHUTES_MODEL_PREF || "chutes/DeepSeek-V3.2-TEE";
this.limits = {
history: this.promptWindowLimit() * 0.15,
system: this.promptWindowLimit() * 0.15,
user: this.promptWindowLimit() * 0.7,
};

this.embedder = embedder ?? new NativeEmbedder();
this.defaultTemp = 0.7;
}

#appendContext(contextTexts = []) {
if (!contextTexts || !contextTexts.length) return "";
return (
"\nContext:\n" +
contextTexts
.map((text, i) => {
return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`;
})
.join("")
);
}

#log(text, ...args) {
console.log(`\x1b[32m[ChutesAI]\x1b[0m ${text}`, ...args);
}

streamingEnabled() {
return "streamGetChatCompletion" in this;
}

static promptWindowLimit(modelName) {
return MODEL_MAP.get("chutes", modelName) ?? 65536;
}

promptWindowLimit() {
return MODEL_MAP.get("chutes", this.model) ?? 65536;
}

async isValidChatCompletionModel(modelName = "") {
return !!modelName;
}

/**
* Generates appropriate content array for a message + attachments.
*/
#generateContent({ userPrompt, attachments = [] }) {
if (!attachments.length) return userPrompt;
const content = [{ type: "text", text: userPrompt }];
for (let attachment of attachments) {
content.push({
type: "image_url",
image_url: {
url: attachment.contentString,
},
});
}
return content.flat();
}

constructPrompt({
systemPrompt = "",
contextTexts = [],
chatHistory = [],
userPrompt = "",
attachments = [],
}) {
const prompt = [
{
role: "system",
content: `${systemPrompt}${this.#appendContext(contextTexts)}`,
},
...chatHistory,
{
role: "user",
content: attachments.length
? this.#generateContent({ userPrompt, attachments })
: userPrompt,
},
];
return prompt;
}

async getChatCompletion(messages = null, { temperature = 0.7 }) {
if (!(await this.isValidChatCompletionModel(this.model)))
throw new Error(
`ChutesAI:chatCompletion: ${this.model} is not valid for chat completion!`
);

const result = await LLMPerformanceMonitor.measureAsyncFunction(
this.openai.chat.completions
.create({
model: this.model,
messages,
temperature,
})
.catch((e) => {
throw new Error(e.message);
})
);

if (
!result.output.hasOwnProperty("choices") ||
result.output.choices.length === 0
)
return null;

return {
textResponse: result.output.choices[0].message.content,
metrics: {
prompt_tokens: result.output.usage?.prompt_tokens || 0,
completion_tokens: result.output.usage?.completion_tokens || 0,
total_tokens: result.output.usage?.total_tokens || 0,
outputTps: result.output.usage?.completion_tokens / (result.output.usage?.completion_time || 1),
duration: result.output.usage?.total_time,
model: this.model,
provider: this.className,
timestamp: new Date(),
},
};
}

async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {
if (!(await this.isValidChatCompletionModel(this.model)))
throw new Error(
`ChutesAI:streamChatCompletion: ${this.model} is not valid for chat completion!`
);

const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({
func: this.openai.chat.completions.create({
model: this.model,
stream: true,
messages,
temperature,
}),
messages,
runPromptTokenCalculation: false,
modelTag: this.model,
provider: this.className,
});
return measuredStreamRequest;
}

handleStream(response, stream, responseProps) {
return handleDefaultStreamResponseV2(response, stream, responseProps);
}

async embedTextInput(textInput) {
return await this.embedder.embedTextInput(textInput);
}

async embedChunks(textChunks = []) {
return await this.embedder.embedChunks(textChunks);
}

async compressMessages(promptArgs = {}, rawHistory = []) {
const { messageArrayCompressor } = require("../../helpers/chat");
const messageArray = this.constructPrompt(promptArgs);
return await messageArrayCompressor(this, messageArray, rawHistory);
}
}

module.exports = {
ChutesLLM,
};
Loading