| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310 |
- // API client for stable-diffusion REST API
- // Type for server config injected by the server
- declare global {
- interface Window {
- __SERVER_CONFIG__?: {
- apiUrl: string;
- apiBasePath: string;
- host: string;
- port: number;
- authMethod: "none" | "unix" | "jwt";
- authEnabled: boolean;
- };
- }
- }
- // Request throttling to prevent excessive API calls
- class RequestThrottler {
- private requests: Map<string, { count: number; resetTime: number }> =
- new Map();
- private maxRequests: number = 10; // Max requests per time window
- private timeWindow: number = 1000; // Time window in milliseconds
- canMakeRequest(key: string): boolean {
- const now = Date.now();
- const request = this.requests.get(key);
- if (!request || now >= request.resetTime) {
- this.requests.set(key, { count: 1, resetTime: now + this.timeWindow });
- return true;
- }
- if (request.count >= this.maxRequests) {
- return false;
- }
- request.count++;
- return true;
- }
- getWaitTime(key: string): number {
- const request = this.requests.get(key);
- if (!request) return 0;
- const now = Date.now();
- if (now >= request.resetTime) return 0;
- return request.resetTime - now;
- }
- }
- // Global throttler instance
- const throttler = new RequestThrottler();
- // Debounce utility for frequent calls
- function _debounce<T extends (...args: unknown[]) => unknown>(
- func: T,
- wait: number,
- immediate?: boolean,
- ): (...args: Parameters<T>) => void {
- let timeout: NodeJS.Timeout | null = null;
- return function executedFunction(...args: Parameters<T>) {
- const later = () => {
- timeout = null;
- if (!immediate) func(...args);
- };
- const callNow = immediate && !timeout;
- if (timeout) clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) func(...args);
- };
- }
- // Cache for API responses to reduce redundant calls
- class ApiCache {
- private cache: Map<string, { data: unknown; timestamp: number; ttl: number }> =
- new Map();
- private defaultTtl: number = 5000; // 5 seconds default TTL
- set(key: string, data: unknown, ttl?: number): void {
- this.cache.set(key, {
- data,
- timestamp: Date.now(),
- ttl: ttl || this.defaultTtl,
- });
- }
- get(key: string): unknown | null {
- const cached = this.cache.get(key);
- if (!cached) return null;
- if (Date.now() - cached.timestamp > cached.ttl) {
- this.cache.delete(key);
- return null;
- }
- return cached.data;
- }
- clear(): void {
- this.cache.clear();
- }
- delete(key: string): void {
- this.cache.delete(key);
- }
- }
- const cache = new ApiCache();
- // Get configuration from server-injected config or fallback to environment/defaults
- // This function is called at runtime to ensure __SERVER_CONFIG__ is available
- function getApiConfig() {
- if (typeof window !== "undefined" && window.__SERVER_CONFIG__) {
- let apiUrl = window.__SERVER_CONFIG__.apiUrl;
- // Fix 0.0.0.0 host to use actual browser host
- if (apiUrl && apiUrl.includes("0.0.0.0")) {
- const protocol = window.location.protocol;
- const host = window.location.hostname;
- const port = window.location.port;
- apiUrl = `${protocol}//${host}${port ? ":" + port : ""}`;
- }
- return {
- apiUrl,
- apiBase: window.__SERVER_CONFIG__.apiBasePath,
- };
- }
- // Fallback for development mode - use current window location
- if (typeof window !== "undefined") {
- const protocol = window.location.protocol;
- const host = window.location.hostname;
- const port = window.location.port;
- return {
- apiUrl: `${protocol}//${host}:${port}`,
- apiBase: "/api",
- };
- }
- // Server-side fallback
- return {
- apiUrl: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8081",
- apiBase: process.env.NEXT_PUBLIC_API_BASE_PATH || "/api",
- };
- }
- export interface GenerationRequest {
- model?: string;
- prompt: string;
- negative_prompt?: string;
- width?: number;
- height?: number;
- steps?: number;
- cfg_scale?: number;
- seed?: string;
- sampling_method?: string;
- scheduler?: string;
- batch_count?: number;
- clip_skip?: number;
- strength?: number;
- control_strength?: number;
- }
- export interface JobInfo {
- id?: string;
- request_id?: string;
- status:
- | "pending"
- | "loading"
- | "processing"
- | "completed"
- | "failed"
- | "cancelled"
- | "queued";
- progress?: number;
- model_load_progress?: number;
- generation_progress?: number;
- overall_progress?: number;
- result?: {
- images: string[];
- };
- outputs?: Array<{
- filename: string;
- url: string;
- path: string;
- }>;
- error?: string;
- created_at?: string;
- updated_at?: string;
- message?: string;
- queue_position?: number;
- prompt?: string;
- end_time?: number;
- start_time?: number;
- queued_time?: number;
- error_message?: string;
- position?: number;
- }
- // API response wrapper for job details
- export interface JobDetailsResponse {
- job: JobInfo;
- }
- export interface ModelInfo {
- id?: string;
- name: string;
- path?: string;
- type: string;
- size?: number;
- file_size?: number;
- file_size_mb?: number;
- sha256?: string | null;
- sha256_short?: string | null;
- loaded?: boolean;
- architecture?: string;
- required_models?: RequiredModelInfo[];
- recommended_vae?: RecommendedModelInfo;
- recommended_textual_inversions?: RecommendedModelInfo[];
- recommended_loras?: RecommendedModelInfo[];
- metadata?: Record<string, unknown>;
- }
- export interface RequiredModelInfo {
- type: string;
- name?: string;
- description?: string;
- optional?: boolean;
- priority?: number;
- }
- export interface RecommendedModelInfo {
- type: string;
- name?: string;
- description?: string;
- reason?: string;
- }
- export interface AutoSelectionState {
- selectedModels: Record<string, string>; // modelType -> modelName
- autoSelectedModels: Record<string, string>; // modelType -> modelName
- missingModels: string[]; // modelType names
- warnings: string[];
- errors: string[];
- isAutoSelecting: boolean;
- }
- export interface EnhancedModelsResponse {
- models: ModelInfo[];
- pagination: {
- page: number;
- limit: number;
- total_count: number;
- total_pages: number;
- has_next: boolean;
- has_prev: boolean;
- };
- statistics: Record<string, unknown>;
- auto_selection?: AutoSelectionState;
- }
- export interface QueueStatus {
- active_generations: number;
- jobs: JobInfo[];
- running: boolean;
- size: number;
- }
- export interface HealthStatus {
- status: "ok" | "error" | "degraded";
- message: string;
- timestamp: string;
- uptime?: number;
- version?: string;
- }
- export interface VersionInfo {
- version: string;
- type: string;
- commit: {
- short: string;
- full: string;
- };
- branch: string;
- clean: boolean;
- build_time: string;
- }
- class ApiClient {
- private baseUrl: string = "";
- private isInitialized: boolean = false;
- // Initialize base URL
- private initBaseUrl(): string {
- if (!this.isInitialized) {
- const config = getApiConfig();
- this.baseUrl = `${config.apiUrl}${config.apiBase}`;
- this.isInitialized = true;
- }
- return this.baseUrl;
- }
- // Get base URL dynamically at runtime to ensure server config is loaded
- private getBaseUrl(): string {
- return this.initBaseUrl();
- }
- private async request<T>(
- endpoint: string,
- options: RequestInit = {},
- ): Promise<T> {
- const url = `${this.getBaseUrl()}${endpoint}`;
- // Check request throttling for certain endpoints
- const needsThrottling =
- endpoint.includes("/queue/status") || endpoint.includes("/health");
- if (needsThrottling) {
- const waitTime = throttler.getWaitTime(endpoint);
- if (waitTime > 0) {
- // Wait before making the request
- await new Promise((resolve) => setTimeout(resolve, waitTime));
- }
- if (!throttler.canMakeRequest(endpoint)) {
- throw new Error(
- "Too many requests. Please wait before making another request.",
- );
- }
- }
- // Get authentication method from server config
- const authMethod =
- typeof window !== "undefined" && window.__SERVER_CONFIG__
- ? window.__SERVER_CONFIG__.authMethod
- : "jwt";
- // Add auth token or Unix user header based on auth method
- const token =
- typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
- const unixUser =
- typeof window !== "undefined" ? localStorage.getItem("unix_user") : null;
- const headers: Record<string, string> = {
- "Content-Type": "application/json",
- ...(options.headers as Record<string, string>),
- };
- if (authMethod === "unix" && unixUser) {
- // For Unix auth, send username in X-Unix-User header
- headers["X-Unix-User"] = unixUser;
- } else if (token) {
- // For JWT auth, send the token in Authorization header
- headers["Authorization"] = `Bearer ${token}`;
- }
- const response = await fetch(url, {
- ...options,
- headers,
- });
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({
- error: { message: response.statusText },
- }));
- // Handle nested error structure: { error: { message: "..." } }
- const errorMessage =
- errorData.error?.message ||
- errorData.message ||
- errorData.error ||
- "API request failed";
- throw new Error(errorMessage);
- }
- return response.json();
- }
- // Enhanced health check with caching and better error handling
- async checkHealth(): Promise<HealthStatus> {
- const cacheKey = "health_check";
- const cachedResult = cache.get(cacheKey) as HealthStatus | null;
- if (cachedResult) {
- return cachedResult;
- }
- const endpoints = ["/queue/status", "/health", "/status", "/"];
- for (const endpoint of endpoints) {
- try {
- const response = await fetch(`${this.getBaseUrl()}${endpoint}`, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- // Add timeout to prevent hanging requests
- signal: AbortSignal.timeout(3000), // Reduced timeout
- });
- if (response.ok) {
- const data = await response.json();
- // For queue status, consider it healthy if it returns valid structure
- if (endpoint === "/queue/status" && data.queue) {
- const result = {
- status: "ok" as const,
- message: "API is running and queue is accessible",
- timestamp: new Date().toISOString(),
- };
- cache.set(cacheKey, result, 10000); // Cache for 10 seconds
- return result;
- }
- // For other health endpoints
- const healthStatus: HealthStatus = {
- status: "ok",
- message: "API is running",
- timestamp: new Date().toISOString(),
- uptime: data.uptime,
- version: data.version || data.build || data.git_version,
- };
- // Handle different response formats
- if (data.status) {
- if (
- data.status === "healthy" ||
- data.status === "running" ||
- data.status === "ok"
- ) {
- healthStatus.status = "ok";
- healthStatus.message = data.message || "API is running";
- } else if (data.status === "degraded") {
- healthStatus.status = "degraded";
- healthStatus.message =
- data.message || "API is running in degraded mode";
- } else {
- healthStatus.status = "error";
- healthStatus.message = data.message || "API status is unknown";
- }
- }
- cache.set(cacheKey, healthStatus, 10000); // Cache for 10 seconds
- return healthStatus;
- }
- } catch (error) {
- // Continue to next endpoint if this one fails
- console.warn(`Health check failed for endpoint ${endpoint}:`, error);
- continue;
- }
- }
- // If all endpoints fail
- throw new Error("All health check endpoints are unavailable");
- }
- // Alternative simple connectivity check with caching
- async checkConnectivity(): Promise<boolean> {
- const cacheKey = "connectivity_check";
- const cachedResult = cache.get(cacheKey) as boolean | undefined;
- if (cachedResult !== undefined) {
- return cachedResult;
- }
- try {
- const response = await fetch(`${this.getBaseUrl()}`, {
- method: "HEAD",
- signal: AbortSignal.timeout(2000), // Reduced timeout
- });
- const result = response.ok || response.status < 500;
- cache.set(cacheKey, result, 5000); // Cache for 5 seconds
- return result;
- } catch {
- cache.set(cacheKey, false, 5000); // Cache failure for 5 seconds
- return false;
- }
- }
- // Generation endpoints
- async generateImage(params: GenerationRequest): Promise<JobInfo> {
- return this.request<JobInfo>("/generate/text2img", {
- method: "POST",
- body: JSON.stringify(params),
- });
- }
- async text2img(params: GenerationRequest): Promise<JobInfo> {
- return this.request<JobInfo>("/generate/text2img", {
- method: "POST",
- body: JSON.stringify(params),
- });
- }
- async img2img(
- params: GenerationRequest & { image: string },
- ): Promise<JobInfo> {
- // Convert frontend field name to backend field name
- const backendParams = {
- ...params,
- init_image: params.image,
- image: undefined, // Remove the frontend field
- };
- return this.request<JobInfo>("/generate/img2img", {
- method: "POST",
- body: JSON.stringify(backendParams),
- });
- }
- async inpainting(
- params: GenerationRequest & { source_image: string; mask_image: string },
- ): Promise<JobInfo> {
- return this.request<JobInfo>("/generate/inpainting", {
- method: "POST",
- body: JSON.stringify(params),
- });
- }
- async upscale(
- params: {
- image: string;
- model: string;
- upscale_factor: number;
- }
- ): Promise<JobInfo> {
- // Map 'model' to 'esrgan_model' as expected by the API
- const apiParams = {
- image: params.image,
- esrgan_model: params.model,
- upscale_factor: params.upscale_factor,
- };
- return this.request<JobInfo>("/generate/upscale", {
- method: "POST",
- body: JSON.stringify(apiParams),
- });
- }
- // Job management with caching for status checks
- async getJobStatus(jobId: string): Promise<JobDetailsResponse> {
- const cacheKey = `job_status_${jobId}`;
- const cachedResult = cache.get(cacheKey) as JobDetailsResponse | null;
- if (cachedResult) {
- return cachedResult;
- }
- const result = await this.request<JobDetailsResponse>(
- `/queue/job/${jobId}`,
- );
- // Cache job status for a short time
- if (result.job.status === "processing" || result.job.status === "queued") {
- cache.set(cacheKey, result, 2000); // Cache for 2 seconds for active jobs
- } else {
- cache.set(cacheKey, result, 10000); // Cache for 10 seconds for completed jobs
- }
- return result;
- }
- // Get authenticated image URL with cache-busting
- getImageUrl(jobId: string, filename: string): string {
- const baseUrl = this.getBaseUrl();
- // Add cache-busting timestamp
- const timestamp = Date.now();
- const url = `${baseUrl}/queue/job/${jobId}/output/${filename}?t=${timestamp}`;
- return url;
- }
- // Download image with authentication
- async downloadImage(jobId: string, filename: string): Promise<Blob> {
- const url = this.getImageUrl(jobId, filename);
- // Get authentication method from server config
- const authMethod =
- typeof window !== "undefined" && window.__SERVER_CONFIG__
- ? window.__SERVER_CONFIG__.authMethod
- : "jwt";
- // Add auth token or Unix user header based on auth method
- const token =
- typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
- const unixUser =
- typeof window !== "undefined" ? localStorage.getItem("unix_user") : null;
- const headers: Record<string, string> = {};
- if (authMethod === "unix" && unixUser) {
- // For Unix auth, send the username in X-Unix-User header
- headers["X-Unix-User"] = unixUser;
- } else if (token) {
- // For JWT auth, send the token in Authorization header
- headers["Authorization"] = `Bearer ${token}`;
- }
- const response = await fetch(url, {
- headers,
- });
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({
- error: { message: response.statusText },
- }));
- // Handle nested error structure: { error: { message: "..." } }
- const errorMessage =
- errorData.error?.message ||
- errorData.message ||
- errorData.error ||
- "Failed to download image";
- throw new Error(errorMessage);
- }
- return response.blob();
- }
- // Download image from URL with server-side proxy to avoid CORS issues
- async downloadImageFromUrl(url: string): Promise<{
- mimeType: string;
- filename: string;
- base64Data: string;
- tempUrl?: string;
- tempFilename?: string;
- }> {
- const apiUrl = `${this.getBaseUrl()}/image/download?url=${encodeURIComponent(url)}`;
- const response = await fetch(apiUrl);
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({
- error: { message: response.statusText },
- }));
- // Handle nested error structure: { error: { message: "..." } }
- const errorMessage =
- errorData.error?.message ||
- errorData.message ||
- errorData.error ||
- "Failed to download image from URL";
- throw new Error(errorMessage);
- }
- const result = await response.json();
- return {
- mimeType: result.mime_type,
- filename: result.filename,
- base64Data: result.base64_data,
- tempUrl: result.temp_url,
- tempFilename: result.temp_filename,
- };
- }
- async cancelJob(jobId: string): Promise<void> {
- // Clear job status cache when cancelling
- cache.delete(`job_status_${jobId}`);
- return this.request<void>("/queue/cancel", {
- method: "POST",
- body: JSON.stringify({ job_id: jobId }),
- });
- }
- // Get queue status with caching and throttling
- async getQueueStatus(): Promise<QueueStatus> {
- const cacheKey = "queue_status";
- const cachedResult = cache.get(cacheKey) as QueueStatus | null;
- if (cachedResult) {
- return cachedResult;
- }
- const response = await this.request<{ queue: QueueStatus }>(
- "/queue/status",
- );
- // Cache queue status based on current activity
- const hasActiveJobs = response.queue.jobs.some(
- (job) => job.status === "processing" || job.status === "queued",
- );
- // Cache for shorter time if there are active jobs
- const cacheTime = hasActiveJobs ? 1000 : 5000; // 1 second for active, 5 seconds for idle
- cache.set(cacheKey, response.queue, cacheTime);
- return response.queue;
- }
- async clearQueue(): Promise<void> {
- // Clear all related caches
- cache.delete("queue_status");
- return this.request<void>("/queue/clear", {
- method: "POST",
- });
- }
- // Model management
- async getModels(
- type?: string,
- loaded?: boolean,
- page: number = 1,
- limit: number = -1,
- search?: string,
- ): Promise<EnhancedModelsResponse> {
- const cacheKey = `models_${type || "all"}_${loaded ? "loaded" : "all"}_${page}_${limit}_${search || "all"}`;
- const cachedResult = cache.get(cacheKey) as EnhancedModelsResponse | null;
- if (cachedResult) {
- return cachedResult;
- }
- let endpoint = "/models";
- const params = [];
- if (type && type !== "loaded") params.push(`type=${type}`);
- if (type === "loaded" || loaded) params.push("loaded=true");
- // Only add page parameter if we're using pagination (limit > 0)
- if (limit > 0) {
- params.push(`page=${page}`);
- params.push(`limit=${limit}`);
- } else {
- // When limit is 0 (default), we want all models, so add limit=0 to disable pagination
- params.push("limit=0");
- }
- if (search) params.push(`search=${encodeURIComponent(search)}`);
- // Add include_metadata for additional information
- params.push("include_metadata=true");
- if (params.length > 0) endpoint += "?" + params.join("&");
- const response = await this.request<EnhancedModelsResponse>(endpoint);
- const models = response.models.map((model) => ({
- ...model,
- id: model.sha256_short || model.sha256 || model.id || model.name,
- size: model.file_size || model.size,
- path: model.path || model.name,
- }));
- const result = {
- ...response,
- models,
- };
- // Cache models for 30 seconds as they don't change frequently
- cache.set(cacheKey, result, 30000);
- return result;
- }
- // Get models with automatic selection information
- async getModelsForAutoSelection(
- checkpointModel?: string,
- ): Promise<EnhancedModelsResponse> {
- const cacheKey = `models_auto_selection_${checkpointModel || "none"}`;
- const cachedResult = cache.get(cacheKey) as EnhancedModelsResponse | null;
- if (cachedResult) {
- return cachedResult;
- }
- let endpoint = "/models";
- const params = [];
- params.push("include_metadata=true");
- params.push("include_requirements=true");
- if (checkpointModel) {
- params.push(`checkpoint=${encodeURIComponent(checkpointModel)}`);
- }
- params.push("limit=0"); // Get all models
- if (params.length > 0) endpoint += "?" + params.join("&");
- const response = await this.request<EnhancedModelsResponse>(endpoint);
- const models = response.models.map((model) => ({
- ...model,
- id: model.sha256_short || model.sha256 || model.id || model.name,
- size: model.file_size || model.size,
- path: model.path || model.name,
- }));
- const result = {
- ...response,
- models,
- };
- // Cache for 30 seconds
- cache.set(cacheKey, result, 30000);
- return result;
- }
- // Utility function to get models by type
- getModelsByType(models: ModelInfo[], type: string): ModelInfo[] {
- return models.filter(
- (model) => model.type.toLowerCase() === type.toLowerCase(),
- );
- }
- // Utility function to find models by name pattern
- findModelsByName(models: ModelInfo[], namePattern: string): ModelInfo[] {
- const pattern = namePattern.toLowerCase();
- return models.filter((model) => model.name.toLowerCase().includes(pattern));
- }
- // Utility function to get loaded models by type
- getLoadedModelsByType(models: ModelInfo[], type: string): ModelInfo[] {
- return this.getModelsByType(models, type).filter((model) => model.loaded);
- }
- // Get all models (for backward compatibility)
- async getAllModels(type?: string, loaded?: boolean): Promise<ModelInfo[]> {
- const allModels: ModelInfo[] = [];
- let page = 1;
- const limit = 100;
- while (true) {
- const response = await this.getModels(type, loaded, page, limit);
- allModels.push(...response.models);
- if (!response.pagination.has_next) {
- break;
- }
- page++;
- }
- return allModels;
- }
- async getModelInfo(modelId: string): Promise<ModelInfo> {
- const cacheKey = `model_info_${modelId}`;
- const cachedResult = cache.get(cacheKey) as ModelInfo | null;
- if (cachedResult) {
- return cachedResult;
- }
- const result = await this.request<ModelInfo>(`/models/${modelId}`);
- cache.set(cacheKey, result, 30000); // Cache for 30 seconds
- return result;
- }
- async loadModel(modelId: string): Promise<void> {
- // Clear model cache when loading
- cache.delete(`model_info_${modelId}`);
- try {
- return await this.request<void>(`/models/${modelId}/load`, {
- method: "POST",
- });
- } catch (error) {
- // Enhance error message for hash validation failures
- const errorMessage = error instanceof Error ? error.message : "Failed to load model";
- if (errorMessage.includes("INVALID_MODEL_IDENTIFIER") || errorMessage.includes("MODEL_NOT_FOUND")) {
- throw new Error(`Hash validation failed: ${errorMessage}. Please ensure you're using a valid model hash instead of a name.`);
- }
- throw error;
- }
- }
- async unloadModel(modelId: string): Promise<void> {
- // Clear model cache when unloading
- cache.delete(`models_*`);
-
- try {
- return await this.request<void>(`/models/${modelId}/unload`, {
- method: "POST",
- });
- } catch (error) {
- // Enhance error message for hash validation failures
- const errorMessage = error instanceof Error ? error.message : "Failed to unload model";
- if (errorMessage.includes("INVALID_MODEL_IDENTIFIER") || errorMessage.includes("MODEL_NOT_FOUND")) {
- throw new Error(`Hash validation failed: ${errorMessage}. Please ensure you're using a valid model hash instead of a name.`);
- }
- throw error;
- }
- }
- async computeModelHash(modelId: string): Promise<{request_id: string, status: string, message: string}> {
- // Clear model cache when computing hash
- cache.delete(`models_*`);
- return this.request<{request_id: string, status: string, message: string}>(`/models/hash`, {
- method: "POST",
- body: JSON.stringify({ models: [modelId] }),
- });
- }
- async scanModels(): Promise<void> {
- // Clear all model caches when scanning
- cache.clear();
- return this.request<void>("/models/refresh", {
- method: "POST",
- });
- }
- async getModelTypes(): Promise<
- Array<{
- type: string;
- description: string;
- extensions: string[];
- capabilities: string[];
- requires?: string[];
- recommended_for: string;
- }>
- > {
- const cacheKey = "model_types";
- const cachedResult = cache.get(cacheKey) as Array<{
- type: string;
- description: string;
- extensions: string[];
- capabilities: string[];
- requires?: string[];
- recommended_for: string;
- }> | null;
- if (cachedResult) {
- return cachedResult;
- }
- const response = await this.request<{
- model_types: Array<{
- type: string;
- description: string;
- extensions: string[];
- capabilities: string[];
- requires?: string[];
- recommended_for: string;
- }>;
- }>("/models/types");
- cache.set(cacheKey, response.model_types, 60000); // Cache for 1 minute
- return response.model_types;
- }
- async convertModel(
- modelName: string,
- quantizationType: string,
- outputPath?: string,
- ): Promise<{ request_id: string; message: string }> {
- return this.request<{ request_id: string; message: string }>(
- "/models/convert",
- {
- method: "POST",
- body: JSON.stringify({
- model_name: modelName,
- quantization_type: quantizationType,
- output_path: outputPath,
- }),
- },
- );
- }
- // System endpoints
- async getHealth(): Promise<{ status: string }> {
- return this.request<{ status: string }>("/health");
- }
- async getStatus(): Promise<Record<string, unknown>> {
- return this.request<Record<string, unknown>>("/status");
- }
- async getSystemInfo(): Promise<Record<string, unknown>> {
- return this.request<Record<string, unknown>>("/system");
- }
- async restartServer(): Promise<{ message: string }> {
- return this.request<{ message: string }>("/system/restart", {
- method: "POST",
- body: JSON.stringify({}),
- });
- }
- // Image manipulation endpoints
- async resizeImage(
- image: string,
- width: number,
- height: number,
- ): Promise<{ image: string }> {
- return this.request<{ image: string }>("/image/resize", {
- method: "POST",
- body: JSON.stringify({
- image,
- width,
- height,
- }),
- });
- }
- async cropImage(
- image: string,
- x: number,
- y: number,
- width: number,
- height: number,
- ): Promise<{ image: string }> {
- return this.request<{ image: string }>("/image/crop", {
- method: "POST",
- body: JSON.stringify({
- image,
- x,
- y,
- width,
- height,
- }),
- });
- }
- // Configuration endpoints with caching
- async getSamplers(): Promise<
- Array<{ name: string; description: string; recommended_steps: number }>
- > {
- const cacheKey = "samplers";
- const cachedResult = cache.get(cacheKey) as Array<{ name: string; description: string; recommended_steps: number }> | null;
- if (cachedResult) {
- return cachedResult;
- }
- const response = await this.request<{
- samplers: Array<{
- name: string;
- description: string;
- recommended_steps: number;
- }>;
- }>("/samplers");
- cache.set(cacheKey, response.samplers, 60000); // Cache for 1 minute
- return response.samplers;
- }
- async getSchedulers(): Promise<Array<{ name: string; description: string }>> {
- const cacheKey = "schedulers";
- const cachedResult = cache.get(cacheKey) as Array<{ name: string; description: string }> | null;
- if (cachedResult) {
- return cachedResult;
- }
- const response = await this.request<{
- schedulers: Array<{ name: string; description: string }>;
- }>("/schedulers");
- cache.set(cacheKey, response.schedulers, 60000); // Cache for 1 minute
- return response.schedulers;
- }
- // Cache management methods
- clearCache(): void {
- cache.clear();
- }
- clearCacheByPrefix(prefix: string): void {
- const keysToDelete: string[] = [];
- // Access the private cache property through type assertion for cleanup
- const cacheInstance = cache as unknown as {
- cache: Map<string, { data: unknown; timestamp: number; ttl: number }>
- };
- cacheInstance.cache.forEach((_: unknown, key: string) => {
- if (key.startsWith(prefix)) {
- keysToDelete.push(key);
- }
- });
- keysToDelete.forEach((key) => cache.delete(key));
- }
- }
- // Generic API request function for authentication
- export async function apiRequest(
- endpoint: string,
- options: RequestInit = {},
- ): Promise<Response> {
- const { apiUrl, apiBase } = getApiConfig();
- const url = `${apiUrl}${apiBase}${endpoint}`;
- // For both Unix and JWT auth, send username and password
- // The server will handle whether password is required based on PAM availability
- // Get authentication method from server config
- const authMethod =
- typeof window !== "undefined" && window.__SERVER_CONFIG__
- ? window.__SERVER_CONFIG__.authMethod
- : "jwt";
- // Add auth token or Unix user header based on auth method
- const token =
- typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
- const unixUser =
- typeof window !== "undefined" ? localStorage.getItem("unix_user") : null;
- const headers: Record<string, string> = {
- "Content-Type": "application/json",
- ...(options.headers as Record<string, string>),
- };
- if (authMethod === "unix" && unixUser) {
- // For Unix auth, send the username in X-Unix-User header
- headers["X-Unix-User"] = unixUser;
- } else if (token) {
- // For JWT auth, send the token in Authorization header
- headers["Authorization"] = `Bearer ${token}`;
- }
- return fetch(url, {
- ...options,
- headers,
- });
- }
- // Authentication API endpoints
- export const authApi = {
- async login(username: string, password?: string) {
- // Get authentication method from server config
- const authMethod =
- typeof window !== "undefined" && window.__SERVER_CONFIG__
- ? window.__SERVER_CONFIG__.authMethod
- : "jwt";
- // For both Unix and JWT auth, send username and password
- // The server will handle whether password is required based on PAM availability
- const response = await apiRequest("/auth/login", {
- method: "POST",
- body: JSON.stringify({ username, password }),
- });
- if (!response.ok) {
- const error = await response
- .json()
- .catch(() => ({ message: "Login failed" }));
- throw new Error(error.message || "Login failed");
- }
- return response.json();
- },
- async validateToken(token: string) {
- const response = await apiRequest("/auth/validate", {
- headers: { Authorization: `Bearer ${token}` },
- });
- if (!response.ok) {
- throw new Error("Token validation failed");
- }
- return response.json();
- },
- async refreshToken() {
- const response = await apiRequest("/auth/refresh", {
- method: "POST",
- });
- if (!response.ok) {
- throw new Error("Token refresh failed");
- }
- return response.json();
- },
- async logout() {
- await apiRequest("/auth/logout", {
- method: "POST",
- });
- },
- async getCurrentUser() {
- const response = await apiRequest("/auth/me");
- if (!response.ok) {
- throw new Error("Failed to get current user");
- }
- return response.json();
- },
- async changePassword(oldPassword: string, newPassword: string) {
- const response = await apiRequest("/auth/change-password", {
- method: "POST",
- body: JSON.stringify({
- old_password: oldPassword,
- new_password: newPassword,
- }),
- });
- if (!response.ok) {
- const error = await response
- .json()
- .catch(() => ({ message: "Password change failed" }));
- throw new Error(error.message || "Password change failed");
- }
- return response.json();
- },
- // API Key management
- async getApiKeys() {
- const response = await apiRequest("/auth/api-keys");
- if (!response.ok) {
- throw new Error("Failed to get API keys");
- }
- return response.json();
- },
- async createApiKey(name: string, scopes?: string[]) {
- const response = await apiRequest("/auth/api-keys", {
- method: "POST",
- body: JSON.stringify({ name, scopes }),
- });
- if (!response.ok) {
- const error = await response
- .json()
- .catch(() => ({ message: "Failed to create API key" }));
- throw new Error(error.message || "Failed to create API key");
- }
- return response.json();
- },
- async deleteApiKey(keyId: string) {
- const response = await apiRequest(`/auth/api-keys/${keyId}`, {
- method: "DELETE",
- });
- if (!response.ok) {
- throw new Error("Failed to delete API key");
- }
- return response.json();
- },
- // User management (admin only)
- async getUsers() {
- const response = await apiRequest("/auth/users");
- if (!response.ok) {
- throw new Error("Failed to get users");
- }
- return response.json();
- },
- async createUser(userData: {
- username: string;
- email?: string;
- password: string;
- role?: string;
- }) {
- const response = await apiRequest("/auth/users", {
- method: "POST",
- body: JSON.stringify(userData),
- });
- if (!response.ok) {
- const error = await response
- .json()
- .catch(() => ({ message: "Failed to create user" }));
- throw new Error(error.message || "Failed to create user");
- }
- return response.json();
- },
- async updateUser(
- userId: string,
- userData: { email?: string; role?: string; active?: boolean },
- ) {
- const response = await apiRequest(`/auth/users/${userId}`, {
- method: "PUT",
- body: JSON.stringify(userData),
- });
- if (!response.ok) {
- const error = await response
- .json()
- .catch(() => ({ message: "Failed to update user" }));
- throw new Error(error.message || "Failed to update user");
- }
- return response.json();
- },
- async deleteUser(userId: string) {
- const response = await apiRequest(`/auth/users/${userId}`, {
- method: "DELETE",
- });
- if (!response.ok) {
- throw new Error("Failed to delete user");
- }
- return response.json();
- },
- };
- // Version API
- export async function getVersion(): Promise<VersionInfo> {
- const response = await apiRequest("/version");
- if (!response.ok) {
- throw new Error("Failed to get version information");
- }
- return response.json();
- }
- export const apiClient = new ApiClient();
|