forked from open-webui/open-webui
feat: litellm frontend integration
This commit is contained in:
parent
de0084c8df
commit
9b6dca3d7f
8 changed files with 134 additions and 62 deletions
42
src/lib/apis/litellm/index.ts
Normal file
42
src/lib/apis/litellm/index.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { LITELLM_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
export const getLiteLLMModels = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${LITELLM_API_BASE_URL}/v1/models`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = Array.isArray(res) ? res : res?.data ?? null;
|
||||||
|
|
||||||
|
return models
|
||||||
|
? models
|
||||||
|
.map((model) => ({
|
||||||
|
id: model.id,
|
||||||
|
name: model.name ?? model.id,
|
||||||
|
external: true,
|
||||||
|
source: 'litellm'
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})
|
||||||
|
: models;
|
||||||
|
};
|
|
@ -128,9 +128,11 @@ export const getOllamaModels = async (token: string = '') => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (res?.models ?? []).sort((a, b) => {
|
return (res?.models ?? [])
|
||||||
return a.name.localeCompare(b.name);
|
.map((model) => ({ id: model.model, name: model.name ?? model.model, ...model }))
|
||||||
});
|
.sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: migrate to backend
|
// TODO: migrate to backend
|
||||||
|
|
|
@ -163,7 +163,7 @@ export const getOpenAIModels = async (token: string = '') => {
|
||||||
|
|
||||||
return models
|
return models
|
||||||
? models
|
? models
|
||||||
.map((model) => ({ name: model.id, external: true }))
|
.map((model) => ({ id: model.id, name: model.name ?? model.id, external: true }))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
})
|
})
|
||||||
|
@ -200,17 +200,21 @@ export const getOpenAIModelsDirect = async (
|
||||||
const models = Array.isArray(res) ? res : res?.data ?? null;
|
const models = Array.isArray(res) ? res : res?.data ?? null;
|
||||||
|
|
||||||
return models
|
return models
|
||||||
.map((model) => ({ name: model.id, external: true }))
|
.map((model) => ({ id: model.id, name: model.name ?? model.id, external: true }))
|
||||||
.filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true))
|
.filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateOpenAIChatCompletion = async (token: string = '', body: object) => {
|
export const generateOpenAIChatCompletion = async (
|
||||||
|
token: string = '',
|
||||||
|
body: object,
|
||||||
|
url: string = OPENAI_API_BASE_URL
|
||||||
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OPENAI_API_BASE_URL}/chat/completions`, {
|
const res = await fetch(`${url}/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
$: if (selectedModels.length > 0 && $models.length > 0) {
|
$: if (selectedModels.length > 0 && $models.length > 0) {
|
||||||
selectedModels = selectedModels.map((model) =>
|
selectedModels = selectedModels.map((model) =>
|
||||||
$models.map((m) => m.name).includes(model) ? model : ''
|
$models.map((m) => m.id).includes(model) ? model : ''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
{#if model.name === 'hr'}
|
{#if model.name === 'hr'}
|
||||||
<hr />
|
<hr />
|
||||||
{:else}
|
{:else}
|
||||||
<option value={model.name} class="text-gray-700 text-lg"
|
<option value={model.id} class="text-gray-700 text-lg"
|
||||||
>{model.name +
|
>{model.name +
|
||||||
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
|
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import { getOllamaModels } from '$lib/apis/ollama';
|
import { getOllamaModels } from '$lib/apis/ollama';
|
||||||
import { getOpenAIModels } from '$lib/apis/openai';
|
import { getOpenAIModels } from '$lib/apis/openai';
|
||||||
|
import { getLiteLLMModels } from '$lib/apis/litellm';
|
||||||
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Account from './Settings/Account.svelte';
|
import Account from './Settings/Account.svelte';
|
||||||
|
@ -41,7 +42,15 @@
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
|
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
|
||||||
|
|
||||||
|
const liteLLMModels = await getLiteLLMModels(localStorage.token).catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
models.push(...(liteLLMModels ? [{ name: 'hr' }, ...liteLLMModels] : []));
|
||||||
}
|
}
|
||||||
|
|
||||||
return models;
|
return models;
|
||||||
|
|
|
@ -5,6 +5,8 @@ export const WEBUI_NAME = 'Open WebUI';
|
||||||
export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
|
export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
|
||||||
|
|
||||||
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
|
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
|
||||||
|
|
||||||
|
export const LITELLM_API_BASE_URL = `${WEBUI_BASE_URL}/litellm/api`;
|
||||||
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`;
|
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`;
|
||||||
export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
|
export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
|
||||||
export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`;
|
export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`;
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
import { getPrompts } from '$lib/apis/prompts';
|
import { getPrompts } from '$lib/apis/prompts';
|
||||||
|
|
||||||
import { getOpenAIModels } from '$lib/apis/openai';
|
import { getOpenAIModels } from '$lib/apis/openai';
|
||||||
|
import { getLiteLLMModels } from '$lib/apis/litellm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
user,
|
user,
|
||||||
|
@ -59,6 +60,13 @@
|
||||||
|
|
||||||
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
|
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
|
||||||
|
|
||||||
|
const liteLLMModels = await getLiteLLMModels(localStorage.token).catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
models.push(...(liteLLMModels ? [{ name: 'hr' }, ...liteLLMModels] : []));
|
||||||
|
|
||||||
return models;
|
return models;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
||||||
import Navbar from '$lib/components/layout/Navbar.svelte';
|
import Navbar from '$lib/components/layout/Navbar.svelte';
|
||||||
import { RAGTemplate } from '$lib/utils/rag';
|
import { RAGTemplate } from '$lib/utils/rag';
|
||||||
|
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
let stopResponseFlag = false;
|
let stopResponseFlag = false;
|
||||||
let autoScroll = true;
|
let autoScroll = true;
|
||||||
|
@ -277,9 +278,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
selectedModels.map(async (model) => {
|
selectedModels.map(async (modelId) => {
|
||||||
console.log(model);
|
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||||
const modelTag = $models.filter((m) => m.name === model).at(0);
|
|
||||||
|
|
||||||
// Create response message
|
// Create response message
|
||||||
let responseMessageId = uuidv4();
|
let responseMessageId = uuidv4();
|
||||||
|
@ -289,7 +289,7 @@
|
||||||
childrenIds: [],
|
childrenIds: [],
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: '',
|
content: '',
|
||||||
model: model,
|
model: model.id,
|
||||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -305,12 +305,12 @@
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelTag?.external) {
|
if (model?.external) {
|
||||||
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
|
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
|
||||||
} else if (modelTag) {
|
} else if (model) {
|
||||||
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
|
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Model ${model} not found`);
|
toast.error(`Model ${model.id} not found`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -319,6 +319,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
|
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
|
||||||
|
model = model.id;
|
||||||
const responseMessage = history.messages[responseMessageId];
|
const responseMessage = history.messages[responseMessageId];
|
||||||
|
|
||||||
// Wait until history/message have been updated
|
// Wait until history/message have been updated
|
||||||
|
@ -530,54 +531,58 @@
|
||||||
const responseMessage = history.messages[responseMessageId];
|
const responseMessage = history.messages[responseMessageId];
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
const res = await generateOpenAIChatCompletion(localStorage.token, {
|
const res = await generateOpenAIChatCompletion(
|
||||||
model: model,
|
localStorage.token,
|
||||||
stream: true,
|
{
|
||||||
messages: [
|
model: model.id,
|
||||||
$settings.system
|
stream: true,
|
||||||
? {
|
messages: [
|
||||||
role: 'system',
|
$settings.system
|
||||||
content: $settings.system
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
...messages
|
|
||||||
]
|
|
||||||
.filter((message) => message)
|
|
||||||
.map((message, idx, arr) => ({
|
|
||||||
role: message.role,
|
|
||||||
...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false
|
|
||||||
? {
|
? {
|
||||||
content: [
|
role: 'system',
|
||||||
{
|
content: $settings.system
|
||||||
type: 'text',
|
|
||||||
text:
|
|
||||||
arr.length - 1 !== idx
|
|
||||||
? message.content
|
|
||||||
: message?.raContent ?? message.content
|
|
||||||
},
|
|
||||||
...message.files
|
|
||||||
.filter((file) => file.type === 'image')
|
|
||||||
.map((file) => ({
|
|
||||||
type: 'image_url',
|
|
||||||
image_url: {
|
|
||||||
url: file.url
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
: {
|
: undefined,
|
||||||
content:
|
...messages
|
||||||
arr.length - 1 !== idx ? message.content : message?.raContent ?? message.content
|
]
|
||||||
})
|
.filter((message) => message)
|
||||||
})),
|
.map((message, idx, arr) => ({
|
||||||
seed: $settings?.options?.seed ?? undefined,
|
role: message.role,
|
||||||
stop: $settings?.options?.stop ?? undefined,
|
...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false
|
||||||
temperature: $settings?.options?.temperature ?? undefined,
|
? {
|
||||||
top_p: $settings?.options?.top_p ?? undefined,
|
content: [
|
||||||
num_ctx: $settings?.options?.num_ctx ?? undefined,
|
{
|
||||||
frequency_penalty: $settings?.options?.repeat_penalty ?? undefined,
|
type: 'text',
|
||||||
max_tokens: $settings?.options?.num_predict ?? undefined
|
text:
|
||||||
});
|
arr.length - 1 !== idx
|
||||||
|
? message.content
|
||||||
|
: message?.raContent ?? message.content
|
||||||
|
},
|
||||||
|
...message.files
|
||||||
|
.filter((file) => file.type === 'image')
|
||||||
|
.map((file) => ({
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: {
|
||||||
|
url: file.url
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
content:
|
||||||
|
arr.length - 1 !== idx ? message.content : message?.raContent ?? message.content
|
||||||
|
})
|
||||||
|
})),
|
||||||
|
seed: $settings?.options?.seed ?? undefined,
|
||||||
|
stop: $settings?.options?.stop ?? undefined,
|
||||||
|
temperature: $settings?.options?.temperature ?? undefined,
|
||||||
|
top_p: $settings?.options?.top_p ?? undefined,
|
||||||
|
num_ctx: $settings?.options?.num_ctx ?? undefined,
|
||||||
|
frequency_penalty: $settings?.options?.repeat_penalty ?? undefined,
|
||||||
|
max_tokens: $settings?.options?.num_predict ?? undefined
|
||||||
|
},
|
||||||
|
model.source === 'litellm' ? `${LITELLM_API_BASE_URL}/v1` : `${OPENAI_API_BASE_URL}`
|
||||||
|
);
|
||||||
|
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
const reader = res.body
|
const reader = res.body
|
||||||
|
|
Loading…
Reference in a new issue