Brama do wszystkich modeli AI. Jak uruchomić LiteLLM na AWS?

Ilość modeli, do których mamy dostęp, oraz to, jak szybko pojawiają się nowe, może przyprawić o zawrót głowy. Szczególnie gdy chcesz mieć najnowsze modele w swojej aplikacji, śledzić koszty i wdrożyć observability. Rozwiązaniem tego problemu może być LLM Gateway.
Czym jest LLM Gateway?
Jest to element infrastruktury, który ma uprościć korzystanie z różnych modeli od różnych dostawców. Zamiast integrować się ze wszystkimi dostawcami osobno, integrujesz się z jednym LLM Gateway, który obsługuje ruch do innych dostawców. Najczęściej LLM Gateway będzie oferować API w formacie kompatybilnym z OpenAI, który jest obsługiwany przez wszystkie biblioteki. Oprócz tego często oferuje dodatkowe funkcjonalności, takie jak:
- analityka
- kontrola kosztów
- konfiguracja limitów
- Guardrails
- i inne.
LiteLLM
LiteLLM jest jednym z najpopularniejszych rozwiązań typu LLM Gateway dostępnych w modelu open-source. Można go uruchomić na swojej infrastrukturze i uprościć zarządzanie różnymi modelami. Poza standardową funkcjonalnością oferuje również:
- Śledzenie kosztów
- Guardrails
- Konfiguracja budżetów
- Rate Limiting
- Observability
LiteLLM można uruchomić w 3 opcjach:
- jako biblioteka Python, która oferuje jednolity interfejs do różnych modeli
- proxy
- w wersji stateless, bez bazy danych, ale również bez niektórych funkcjonalności
- z bazą danych i wszystkimi funkcjonalnościami
To, co podoba mi się w LiteLLM, to bogata dokumentacja, obszerny plik konfiguracyjny oraz przystępny panel admina.
Kiedy warto zastosować LiteLLM?
Jest to ciekawe rozwiązanie, ale dodaje dodatkowy narzut oraz konieczność utrzymywania dodatkowej infrastruktury. Widzę 2 główne zastosowania dla tego rozwiązania:
- W aplikacji korzystasz z różnych modeli od różnych dostawców i chcesz tym zarządzać w jednym miejscu.
- Chcesz dać dostęp do modeli różnym osobom, ale nie chcesz dawać bezpośrednio do dostawców. Jest to ciekawa opcja dla firm, które chcą uprościć zarządzanie dostępami do modeli i optymalizować koszty przez nadanie limitów.
Przykład użycia LiteLLM z AI SDK
Tak jak pisałem wyżej, LiteLLM wystawia zgodne API z OpenAI. Dzięki temu dowolny klient OpenAI zadziała. AI SDK ma osobną bibliotekę dla takiej konfiguracji, którą można użyć.
1npm i @ai-sdk/openai-compatible
21import { generateText } from 'ai';
2
3import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
4
5const litellm = createOpenAICompatible({
6 name: 'litellm',
7 baseURL: 'http://localhost:4000/v1',
8 apiKey: 'sk-XYZ'
9});
10
11const completion = await generateText({
12 prompt: "Tell me a joke",
13 model: litellm('anthropic.claude-3-5-sonnet-20240620-v1:0')
14});
15
16console.log(completion);
17Najważniejsze to podmienić baseURL na właściwy oraz dodać apiKey. Reszta wygląda jak korzystanie ze wszystkich innych modeli.
Deploy LiteLLM na AWS AppRunner z AWS CDK
LiteLLM udostępnia obraz Dockerowy, który umożliwia uruchomienie tego rozwiązania w dowolnym miejscu. W oficjalnej dokumentacji znajdziesz instrukcje, jak uruchomić to w AWS EKS (Kubernetes) lub bezpośrednio na EC2. Ja wolę AppRunner, bo jest prostszy dla mniej doświadczonych użytkowników. Uruchomienie jest banalnie proste, o ile nie popełnisz błędów, które ja popełniłem za pierwszym razem.
Architektura
LiteLLM potrzebuje kilku elementów, by poprawnie działać:
- AWS S3 - do trzymania pliku konfiguracyjnego
- AWS ECR - do trzymania obrazu Dockerowego
- AWS Bedrock - jako dostawca modeli (ale można użyć innych dostawców)
- AWS AppRunner - jako serwis do uruchomienia obrazu dockerowego
- AWS IAM - musimy dodać rolę dla instancji AppRunner, by była w stanie dostać się do S3 oraz uruchamiać modele w Bedrock
- AWS RDS - baza danych, ale można użyć czegoś innego, np.: Supabase
Wszystko to można konfigurować ręcznie, ale jest to narażone na błędy i lepiej wykorzystać podejście IaC do zautomatyzowania tego.
LiteLLM IaC z AWS CDK
Ja wykorzystałem AWS CDK, ale możesz użyć dowolnego rozwiązania dla IaC, jak Pulumi albo Terraform. Niezależnie od wykorzystanego narzędzia trzeba zrobić:
- Stworzyć bucket S3
- Zrobić upload pliku konfiguracyjnego (w momencie uruchomienia aplikacji na AppRunner plik musi być dostępny)
- Stworzyć rolę dla AppRunner z dostępem do ECR (accessRole)
- Stworzyć rolę dla AppRunner z dostępem do S3 i Bedrock (instanceRole)
- Stworzyć instancję AppRunner z odpowiednimi zmiennymi
- DATABASE_URL - db url do bazy danych
- LITELLM_CONFIG_BUCKET_NAME - nazwa bucketu utworzonego w kroku 1
- LITELLM_CONFIG_BUCKET_OBJECT_KEY - nazwa pliku konfiguracyjnego *yaml
- LITELLM_MASTER_KEY - klucz master w formacie sk-XXX
- STORE_MODEL_IN_DB - ’True’ by zapisywać konfiguracje w bazie
Konfiguracja krok po kroku
Na start warto skorzystać z oficjalnego CLI od AWS CDK, które stworzy projekt z odpowiednimi plikami
1mkdir litellm
2cd litellm
3cdk init app --language=typescript
4Następnie w katalogu lib będzie plik litellm-stack.ts, który trzeba zmodyfikować
1export interface LitellmStackProps extends cdk.StackProps {
2 ecrRepositoryName: string;
3 imageTag?: string;
4 databaseUrl: string;
5 litellmMasterKey: string;
6}
7
8export class LitellmStack extends cdk.Stack {
9
10 constructor(scope: Construct, id: string, props: LitellmStackProps) {
11 super(scope, id, props);
12 }
13}
14Wszystkie operacje można wykonać w konstruktorze albo rozbić na więcej metod. Z racji tego, że nie potrzebuję tutaj dużej reużywalności elementów, to wszystko wrzuciłem do konstruktora.
Stworzenie bucketu na S3
1const configBucket = new s3.Bucket(this, 'LitellmConfigBucket', {
2 bucketName: `litellm-config-${this.account}-${this.region}`,
3 removalPolicy: cdk.RemovalPolicy.DESTROY,
4 autoDeleteObjects: true, // For development/testing
5});
6Tutaj nie ma dużej filozofii. Przy tworzeniu bucketa jedynie nazwa jest wymagana, a resztę możemy zostawić jako domyślne. Ja tutaj jedynie dodałem opcję, by podczas usuwania stack’a usuwało też cały bucket.
Upload pliku z konfiguracją
1const configDeployment = new s3deploy.BucketDeployment(this, 'ConfigDeployment', {
2 sources: [s3deploy.Source.asset('./config')],
3 destinationBucket: configBucket,
4 retainOnDelete: false,
5});
6Przy uploadzie należy zwrócić uwagę na poprawną ścieżkę do plików. Ja tutaj synchronizuję cały folder config. Ścieżka jest relatywna w stosunku do pliku package.json, a nie w stosunku do pliku litellm-stack.
Tworzenie roli dla AppRunner z dostępem do ECR (accessRole)
1const accessRole = new iam.Role(this, 'AppRunnerAccessRole', {
2 assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'),
3 description: 'Role for App Runner to access ECR',
4});
5
6accessRole.addToPolicy(
7 new iam.PolicyStatement({
8 effect: iam.Effect.ALLOW,
9 actions: [
10 'ecr:GetAuthorizationToken',
11 'ecr:BatchCheckLayerAvailability',
12 'ecr:GetDownloadUrlForLayer',
13 'ecr:BatchGetImage',
14 ],
15 resources: ['*'],
16 })
17);
18Jest to rola wykorzystywana podczas etapu tworzenia nowej instancji AppRunner i tutaj potrzebny jest jedynie dostęp do ECR, skąd można pobrać obraz Docker’a.
Tworzenie roli dla AppRunner z dostępem do S3 i Bedrock (instanceRole)
1const appRunnerRole = new iam.Role(this, 'AppRunnerServiceRole', {
2 assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'),
3 description: 'Role for LiteLLM App Runner service',
4});
5
6configBucket.grantRead(appRunnerRole);
7
8appRunnerRole.addToPolicy(
9 new iam.PolicyStatement({
10 effect: iam.Effect.ALLOW,
11 actions: [
12 'bedrock:InvokeModel',
13 'bedrock:InvokeModelWithResponseStream',
14 ],
15 resources: ['*'], // You might want to restrict this to specific models
16 })
17);
18Z kolei ta rola jest wymagana wyłącznie dla uruchomionej instancji AppRunner i tutaj jest potrzebny dostęp do S3 (by pobrać konfigurację) oraz Bedrock, by uruchamiać modele.
Konfiguracja AppRunner
1const appRunnerService = new apprunner.Service(this, 'LitellmAppRunnerService', {
2 serviceName: 'ap-litellm-service',
3 source: apprunner.Source.fromEcr({
4 imageConfiguration: {
5 port: 4000, // Default LiteLLM port
6 environmentVariables: {
7 DATABASE_URL: props.databaseUrl,
8 LITELLM_CONFIG_BUCKET_NAME: configBucket.bucketName,
9 LITELLM_CONFIG_BUCKET_OBJECT_KEY: 'litellm-config.yaml',
10 LITELLM_MASTER_KEY: props.litellmMasterKey,
11 STORE_MODEL_IN_DB: 'True',
12 },
13 },
14 repository: ecrRepository,
15 tagOrDigest: props.imageTag || 'latest',
16 }),
17 accessRole: accessRole,
18 instanceRole: appRunnerRole,
19 cpu: apprunner.Cpu.TWO_VCPU,
20 memory: apprunner.Memory.FOUR_GB,
21 autoDeploymentsEnabled: false,
22});
23Ostatnia rzecz to stworzenie odpowiedniej instancji AppRunner z wszystkimi wcześniej skonfigurowanymi elementami.
LiteLLM Deploy Troubleshooting
Błąd połączenia z bazą danych
Sprawdź, czy AppRunner ma dostęp do bazy danych. Jeśli masz RDS’a skonfigurowanego poprawnie, to pewnie jest w prywatnym VPC. Musisz się upewnić, że AppRunner ma właściwą konfigurację dla Egress.
W przypadku Supabase zadziałał mi dopiero Session pooler. Obstawiam, że to kwestia wsparcia dla IPv4, ale nie zgłębiałem bardziej tego tematu.
W przypadku baz danych hostowanych gdzieś indziej warto upewnić się, że URL jest poprawny.
Błąd z plikiem konfiguracyjnym
Upewnij się, że AppRunner ma dostęp do pliku w momencie uruchamiania i plik nie jest pusty. Pusty plik jest traktowany tak, jakby go nie było. Jeśli nie potrzebujesz nic konfigurować, to wrzuć poniższą zawartość
1model_list:
2

