AI funkcionalnost može da bude mali HTTP zahtev, ali i model koji radi na grafičkom procesoru korisnika. Troškovi pokretanja ta dva pristupa znatno se razlikuju. Ako učitamo oba čim se aplikacija otvori, korisnik preuzima i funkcionalnost koju možda nikada neće koristiti.
Demo ng-switzerland istražuje taj problem uz Angular 22: jedan chat interfejs, dva provajdera i jasna granica između izbora provajdera i njegovog učitavanja. OpenAI poziva zaseban NestJS API. Lokalni AI koristi WebLLM i Llama 3.2 1B model u Web Worker-u.
Jedan ugovor, dva načina izvršavanja
Interfejs treba da pošalje prompt i prikaže odgovor. Detalji HTTP adaptera i inicijalizacije lokalnog modela ostaju u provajderima. Oba implementiraju isti TypeScript interfejs:
export interface AiProvider {
chat(prompt: string, model?: string, onProgress?: (status: string) => void): Promise<string>;
}
export type AiProviderId = 'openai' | 'local';
export type AiProviderLoader = () => Promise<AiProvider>;
export const AI_PROVIDER_OPTIONS = [
{ id: 'openai', label: 'OpenAI' },
{ id: 'local', label: 'Local AI' },
] as const satisfies readonly { id: AiProviderId; label: string }[];
export const OPENAI_MODELS = [
{ id: 'gpt-4.1-mini', label: 'GPT-4.1 mini' },
{ id: 'gpt-5-mini', label: 'GPT-5 mini' },
] as const;
export const LOCAL_MODEL = 'Llama-3.2-1B-Instruct-q4f16_1-MLC';
Callback za napredak omogućava lokalnom provajderu da prijavi inicijalizaciju bez zavisnosti od UI stanja. Interfejs ne postoji tokom izvršavanja; injection token obezbeđuje konkretnu funkciju za učitavanje provajdera.
Prvo dobijamo loader, zatim učitavamo servis
Važan detalj Angular injectAsync API-ja jeste povratna vrednost: funkcija koja asinhrono razrešava zavisnost. Demo kreira te funkcije unutar injection konteksta, a computed signal bira jednu od njih:
import { computed, inject, injectAsync, InjectionToken, type Signal } from '@angular/core';
import type { AiProvider, AiProviderId, AiProviderLoader } from './ai-provider';
import { AI_PROVIDER_SELECTION } from './ai-provider-selection';
export const AI_PROVIDER_LOADER = new InjectionToken<Signal<AiProviderLoader>>(
'AI_PROVIDER_LOADER',
);
export function createAiProviderLoader(): Signal<AiProviderLoader> {
const selection = inject(AI_PROVIDER_SELECTION);
const loaders = {
openai: injectAsync<AiProvider>(() =>
import('./open-ai-provider').then((m) => m.OpenAiProvider),
),
local: injectAsync<AiProvider>(() =>
import('./local-ai-provider').then((m) => m.LocalAiProvider),
),
} satisfies Record<AiProviderId, AiProviderLoader>;
return computed(() => loaders[selection()]);
}
Promena izbora menja funkciju koju signal vraća. Tek pozivanje te funkcije traži izabranu zavisnost. Zato izbor Local AI opcije sam po sebi ne inicijalizuje model.
Konkretni provajderi u ovom Angular 22 projektu koriste @Service() za automatsko obezbeđivanje. Njihovi importi ostaju dinamički, dok zajednički ugovor koristi type-only importe. Statički import ili registracija konkretnog provajdera na drugom mestu mogu da naruše planiranu granicu učitavanja. Zahtevi framework-a opisani su u Angular vodiču za lazy učitavanje servisa.
Sačuvajte izbor pre await-a
Korisnik može da promeni provajdera dok zahtev traje. Novi izbor treba da važi za sledeći zahtev, dok tekući nastavlja sa provajderom sa kojim je započet. Fasada zato sinhrono preuzima loader:
import { inject, Injectable } from '@angular/core';
import type { AiProvider } from './ai-provider';
import { AI_PROVIDER_LOADER } from './ai-provider-loader';
@Injectable()
export class Ai implements AiProvider {
readonly #provider = inject(AI_PROVIDER_LOADER);
async chat(
prompt: string,
model?: string,
onProgress?: (status: string) => void,
): Promise<string> {
// Capture the selection before awaiting: a pending request keeps its provider.
const loadProvider = this.#provider();
const provider = await loadProvider();
return provider.chat(prompt, model, onProgress);
}
}
Redosled je precizan: izbor, preuzimanje loader-a, čekanje, izvršavanje. Inicijalizaciju modela i generisanje odgovora fasada prepušta izabranom provajderu.
Scope je deo dizajna
provideAi() zajedno registruje fasadu, signal izbora i loader. Chat komponenta dobija sopstveni lanac izbora preko provideAiChat(), pa dve chat instance mogu da koriste različite provajdere.
Konkretni adapteri imaju root scope. OpenAI adapter je bez stanja, dok lokalni provajder upravlja runtime-om koji se ponovo koristi. Deljenje lokalnog runtime-a sprečava stvaranje zasebnog GPU engine-a za svaki chat, ali zahteva pravilo za paralelne zahteve. Demo odbija novi lokalni zahtev dok je runtime zauzet.
Učitavanje JavaScript-a nije učitavanje modela
Lokalni AI prolazi kroz nekoliko faza: učitavanje provajdera, import WebLLM-a, pokretanje worker-a, preuzimanje modela i inicijalizaciju engine-a. Provajder kreira runtime prvim pozivom chat() metode, a zatim ga koristi za naredne zahteve.
Prva inicijalizacija može da preuzme stotine megabajta. Potrebni su WebGPU podrška i dovoljno GPU memorije. Composer prikazuje napredak, a inicijalizacija i generisanje imaju timeout od pet minuta. Greška resetuje runtime za novi pokušaj; uništavanje servisa prekida worker.
Demo koristi kontekst od 4096 tokena i ograničava odgovor na 512 tokena. Svaki prompt je nezavisan: istorija prikazana u interfejsu ne šalje se modelu kao kontekst razgovora. Sam izgled chat-a ne znači da model pamti prethodne poruke.
PWA mora da poštuje granicu učitavanja
Dinamički import može da odloži izvršavanje, dok service worker ipak unapred preuzima njegov chunk. U ovom projektu glavni resursi aplikacije koriste prefetch instalaciju, a preostali JavaScript resursi lazy instalaciju i ažuriranje.
Tako lokalni engine ostaje van početnog preuzimanja. To nije garancija offline izvršavanja. WebLLM zasebno upravlja kešom modela, pa offline rad zavisi od dostupnosti svih potrebnih resursa. Brisanje podataka iz browser skladišta može da zahteva novo preuzimanje.
Početna stranica se prerenderuje, a hidratacija se izvršava u browser-u. GPU se koristi tek na zahtev za lokalno generisanje. Pravi OpenAI pozivi zahtevaju mrežu i konfigurisan backend; produkciona konfiguracija frontend-a trenutno koristi mock režim dok se ne postavi URL hostovanog API-ja. API kredencijali ostaju na serveru.
Proverite ponašanje u Network panelu
- Otvorite novu browser sesiju i očistite mrežni log.
- Pošaljite OpenAI prompt i proverite da se lokalni runtime i model ne preuzimaju.
- Izaberite Local AI i proverite da inicijalizacija još nije počela.
- Pošaljite lokalni prompt i pratite učitavanje provajdera, worker-a, runtime-a i modela.
- Pošaljite naredni lokalni prompt i proverite ponovno korišćenje runtime-a.
Automatizovani testovi projekta koriste test doubles za izbor provajdera, nezavisne chat instance, HTTP greške i životni ciklus lokalnog runtime-a. Oni ne dokazuju GPU izvršavanje na svakom uređaju. To treba posebno proveriti na browser-ima i hardveru koji će se koristiti.
Šta ovaj dizajn donosi
Najvažnija granica jeste trenutak kada skupa funkcionalnost postaje potrebna. U ovom demou izbor je jeftin, servis se učitava na zahtev, a inicijalizacijom modela upravlja njegov runtime. Takva podela omogućava da isti interfejs koristi mali cloud adapter i zahtevniji lokalni engine, bez obaveznog učitavanja oba za svakog korisnika.