api.ts 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309
  1. // API client for stable-diffusion REST API
  2. // Type for server config injected by the server
  3. declare global {
  4. interface Window {
  5. __SERVER_CONFIG__?: {
  6. apiUrl: string;
  7. apiBasePath: string;
  8. host: string;
  9. port: number;
  10. authMethod: "none" | "unix" | "jwt";
  11. authEnabled: boolean;
  12. };
  13. }
  14. }
  15. // Request throttling to prevent excessive API calls
  16. class RequestThrottler {
  17. private requests: Map<string, { count: number; resetTime: number }> =
  18. new Map();
  19. private maxRequests: number = 10; // Max requests per time window
  20. private timeWindow: number = 1000; // Time window in milliseconds
  21. canMakeRequest(key: string): boolean {
  22. const now = Date.now();
  23. const request = this.requests.get(key);
  24. if (!request || now >= request.resetTime) {
  25. this.requests.set(key, { count: 1, resetTime: now + this.timeWindow });
  26. return true;
  27. }
  28. if (request.count >= this.maxRequests) {
  29. return false;
  30. }
  31. request.count++;
  32. return true;
  33. }
  34. getWaitTime(key: string): number {
  35. const request = this.requests.get(key);
  36. if (!request) return 0;
  37. const now = Date.now();
  38. if (now >= request.resetTime) return 0;
  39. return request.resetTime - now;
  40. }
  41. }
  42. // Global throttler instance
  43. const throttler = new RequestThrottler();
  44. // Debounce utility for frequent calls
  45. function _debounce<T extends (...args: unknown[]) => unknown>(
  46. func: T,
  47. wait: number,
  48. immediate?: boolean,
  49. ): (...args: Parameters<T>) => void {
  50. let timeout: NodeJS.Timeout | null = null;
  51. return function executedFunction(...args: Parameters<T>) {
  52. const later = () => {
  53. timeout = null;
  54. if (!immediate) func(...args);
  55. };
  56. const callNow = immediate && !timeout;
  57. if (timeout) clearTimeout(timeout);
  58. timeout = setTimeout(later, wait);
  59. if (callNow) func(...args);
  60. };
  61. }
  62. // Cache for API responses to reduce redundant calls
  63. class ApiCache {
  64. private cache: Map<string, { data: unknown; timestamp: number; ttl: number }> =
  65. new Map();
  66. private defaultTtl: number = 5000; // 5 seconds default TTL
  67. set(key: string, data: unknown, ttl?: number): void {
  68. this.cache.set(key, {
  69. data,
  70. timestamp: Date.now(),
  71. ttl: ttl || this.defaultTtl,
  72. });
  73. }
  74. get(key: string): unknown | null {
  75. const cached = this.cache.get(key);
  76. if (!cached) return null;
  77. if (Date.now() - cached.timestamp > cached.ttl) {
  78. this.cache.delete(key);
  79. return null;
  80. }
  81. return cached.data;
  82. }
  83. clear(): void {
  84. this.cache.clear();
  85. }
  86. delete(key: string): void {
  87. this.cache.delete(key);
  88. }
  89. }
  90. const cache = new ApiCache();
  91. // Get configuration from server-injected config or fallback to environment/defaults
  92. // This function is called at runtime to ensure __SERVER_CONFIG__ is available
  93. function getApiConfig() {
  94. if (typeof window !== "undefined" && window.__SERVER_CONFIG__) {
  95. let apiUrl = window.__SERVER_CONFIG__.apiUrl;
  96. // Fix 0.0.0.0 host to use actual browser host
  97. if (apiUrl && apiUrl.includes("0.0.0.0")) {
  98. const protocol = window.location.protocol;
  99. const host = window.location.hostname;
  100. const port = window.location.port;
  101. apiUrl = `${protocol}//${host}${port ? ":" + port : ""}`;
  102. }
  103. return {
  104. apiUrl,
  105. apiBase: window.__SERVER_CONFIG__.apiBasePath,
  106. };
  107. }
  108. // Fallback for development mode - use current window location
  109. if (typeof window !== "undefined") {
  110. const protocol = window.location.protocol;
  111. const host = window.location.hostname;
  112. const port = window.location.port;
  113. return {
  114. apiUrl: `${protocol}//${host}:${port}`,
  115. apiBase: "/api",
  116. };
  117. }
  118. // Server-side fallback
  119. return {
  120. apiUrl: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8081",
  121. apiBase: process.env.NEXT_PUBLIC_API_BASE_PATH || "/api",
  122. };
  123. }
  124. export interface GenerationRequest {
  125. model?: string;
  126. prompt: string;
  127. negative_prompt?: string;
  128. width?: number;
  129. height?: number;
  130. steps?: number;
  131. cfg_scale?: number;
  132. seed?: string;
  133. sampling_method?: string;
  134. scheduler?: string;
  135. batch_count?: number;
  136. clip_skip?: number;
  137. strength?: number;
  138. control_strength?: number;
  139. }
  140. export interface JobInfo {
  141. id?: string;
  142. request_id?: string;
  143. status:
  144. | "pending"
  145. | "loading"
  146. | "processing"
  147. | "completed"
  148. | "failed"
  149. | "cancelled"
  150. | "queued";
  151. progress?: number;
  152. model_load_progress?: number;
  153. generation_progress?: number;
  154. result?: {
  155. images: string[];
  156. };
  157. outputs?: Array<{
  158. filename: string;
  159. url: string;
  160. path: string;
  161. }>;
  162. error?: string;
  163. created_at?: string;
  164. updated_at?: string;
  165. message?: string;
  166. queue_position?: number;
  167. prompt?: string;
  168. end_time?: number;
  169. start_time?: number;
  170. queued_time?: number;
  171. error_message?: string;
  172. position?: number;
  173. }
  174. // API response wrapper for job details
  175. export interface JobDetailsResponse {
  176. job: JobInfo;
  177. }
  178. export interface ModelInfo {
  179. id?: string;
  180. name: string;
  181. path?: string;
  182. type: string;
  183. size?: number;
  184. file_size?: number;
  185. file_size_mb?: number;
  186. sha256?: string | null;
  187. sha256_short?: string | null;
  188. loaded?: boolean;
  189. architecture?: string;
  190. required_models?: RequiredModelInfo[];
  191. recommended_vae?: RecommendedModelInfo;
  192. recommended_textual_inversions?: RecommendedModelInfo[];
  193. recommended_loras?: RecommendedModelInfo[];
  194. metadata?: Record<string, unknown>;
  195. }
  196. export interface RequiredModelInfo {
  197. type: string;
  198. name?: string;
  199. description?: string;
  200. optional?: boolean;
  201. priority?: number;
  202. }
  203. export interface RecommendedModelInfo {
  204. type: string;
  205. name?: string;
  206. description?: string;
  207. reason?: string;
  208. }
  209. export interface AutoSelectionState {
  210. selectedModels: Record<string, string>; // modelType -> modelName
  211. autoSelectedModels: Record<string, string>; // modelType -> modelName
  212. missingModels: string[]; // modelType names
  213. warnings: string[];
  214. errors: string[];
  215. isAutoSelecting: boolean;
  216. }
  217. export interface EnhancedModelsResponse {
  218. models: ModelInfo[];
  219. pagination: {
  220. page: number;
  221. limit: number;
  222. total_count: number;
  223. total_pages: number;
  224. has_next: boolean;
  225. has_prev: boolean;
  226. };
  227. statistics: Record<string, unknown>;
  228. auto_selection?: AutoSelectionState;
  229. }
  230. export interface QueueStatus {
  231. active_generations: number;
  232. jobs: JobInfo[];
  233. running: boolean;
  234. size: number;
  235. }
  236. export interface HealthStatus {
  237. status: "ok" | "error" | "degraded";
  238. message: string;
  239. timestamp: string;
  240. uptime?: number;
  241. version?: string;
  242. }
  243. export interface VersionInfo {
  244. version: string;
  245. type: string;
  246. commit: {
  247. short: string;
  248. full: string;
  249. };
  250. branch: string;
  251. clean: boolean;
  252. build_time: string;
  253. }
  254. class ApiClient {
  255. private baseUrl: string = "";
  256. private isInitialized: boolean = false;
  257. // Initialize base URL
  258. private initBaseUrl(): string {
  259. if (!this.isInitialized) {
  260. const config = getApiConfig();
  261. this.baseUrl = `${config.apiUrl}${config.apiBase}`;
  262. this.isInitialized = true;
  263. }
  264. return this.baseUrl;
  265. }
  266. // Get base URL dynamically at runtime to ensure server config is loaded
  267. private getBaseUrl(): string {
  268. return this.initBaseUrl();
  269. }
  270. private async request<T>(
  271. endpoint: string,
  272. options: RequestInit = {},
  273. ): Promise<T> {
  274. const url = `${this.getBaseUrl()}${endpoint}`;
  275. // Check request throttling for certain endpoints
  276. const needsThrottling =
  277. endpoint.includes("/queue/status") || endpoint.includes("/health");
  278. if (needsThrottling) {
  279. const waitTime = throttler.getWaitTime(endpoint);
  280. if (waitTime > 0) {
  281. // Wait before making the request
  282. await new Promise((resolve) => setTimeout(resolve, waitTime));
  283. }
  284. if (!throttler.canMakeRequest(endpoint)) {
  285. throw new Error(
  286. "Too many requests. Please wait before making another request.",
  287. );
  288. }
  289. }
  290. // Get authentication method from server config
  291. const authMethod =
  292. typeof window !== "undefined" && window.__SERVER_CONFIG__
  293. ? window.__SERVER_CONFIG__.authMethod
  294. : "jwt";
  295. // Add auth token or Unix user header based on auth method
  296. const token =
  297. typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
  298. const unixUser =
  299. typeof window !== "undefined" ? localStorage.getItem("unix_user") : null;
  300. const headers: Record<string, string> = {
  301. "Content-Type": "application/json",
  302. ...(options.headers as Record<string, string>),
  303. };
  304. if (authMethod === "unix" && unixUser) {
  305. // For Unix auth, send username in X-Unix-User header
  306. headers["X-Unix-User"] = unixUser;
  307. } else if (token) {
  308. // For JWT auth, send the token in Authorization header
  309. headers["Authorization"] = `Bearer ${token}`;
  310. }
  311. const response = await fetch(url, {
  312. ...options,
  313. headers,
  314. });
  315. if (!response.ok) {
  316. const errorData = await response.json().catch(() => ({
  317. error: { message: response.statusText },
  318. }));
  319. // Handle nested error structure: { error: { message: "..." } }
  320. const errorMessage =
  321. errorData.error?.message ||
  322. errorData.message ||
  323. errorData.error ||
  324. "API request failed";
  325. throw new Error(errorMessage);
  326. }
  327. return response.json();
  328. }
  329. // Enhanced health check with caching and better error handling
  330. async checkHealth(): Promise<HealthStatus> {
  331. const cacheKey = "health_check";
  332. const cachedResult = cache.get(cacheKey) as HealthStatus | null;
  333. if (cachedResult) {
  334. return cachedResult;
  335. }
  336. const endpoints = ["/queue/status", "/health", "/status", "/"];
  337. for (const endpoint of endpoints) {
  338. try {
  339. const response = await fetch(`${this.getBaseUrl()}${endpoint}`, {
  340. method: "GET",
  341. headers: {
  342. "Content-Type": "application/json",
  343. },
  344. // Add timeout to prevent hanging requests
  345. signal: AbortSignal.timeout(3000), // Reduced timeout
  346. });
  347. if (response.ok) {
  348. const data = await response.json();
  349. // For queue status, consider it healthy if it returns valid structure
  350. if (endpoint === "/queue/status" && data.queue) {
  351. const result = {
  352. status: "ok" as const,
  353. message: "API is running and queue is accessible",
  354. timestamp: new Date().toISOString(),
  355. };
  356. cache.set(cacheKey, result, 10000); // Cache for 10 seconds
  357. return result;
  358. }
  359. // For other health endpoints
  360. const healthStatus: HealthStatus = {
  361. status: "ok",
  362. message: "API is running",
  363. timestamp: new Date().toISOString(),
  364. uptime: data.uptime,
  365. version: data.version || data.build || data.git_version,
  366. };
  367. // Handle different response formats
  368. if (data.status) {
  369. if (
  370. data.status === "healthy" ||
  371. data.status === "running" ||
  372. data.status === "ok"
  373. ) {
  374. healthStatus.status = "ok";
  375. healthStatus.message = data.message || "API is running";
  376. } else if (data.status === "degraded") {
  377. healthStatus.status = "degraded";
  378. healthStatus.message =
  379. data.message || "API is running in degraded mode";
  380. } else {
  381. healthStatus.status = "error";
  382. healthStatus.message = data.message || "API status is unknown";
  383. }
  384. }
  385. cache.set(cacheKey, healthStatus, 10000); // Cache for 10 seconds
  386. return healthStatus;
  387. }
  388. } catch (error) {
  389. // Continue to next endpoint if this one fails
  390. console.warn(`Health check failed for endpoint ${endpoint}:`, error);
  391. continue;
  392. }
  393. }
  394. // If all endpoints fail
  395. throw new Error("All health check endpoints are unavailable");
  396. }
  397. // Alternative simple connectivity check with caching
  398. async checkConnectivity(): Promise<boolean> {
  399. const cacheKey = "connectivity_check";
  400. const cachedResult = cache.get(cacheKey) as boolean | undefined;
  401. if (cachedResult !== undefined) {
  402. return cachedResult;
  403. }
  404. try {
  405. const response = await fetch(`${this.getBaseUrl()}`, {
  406. method: "HEAD",
  407. signal: AbortSignal.timeout(2000), // Reduced timeout
  408. });
  409. const result = response.ok || response.status < 500;
  410. cache.set(cacheKey, result, 5000); // Cache for 5 seconds
  411. return result;
  412. } catch {
  413. cache.set(cacheKey, false, 5000); // Cache failure for 5 seconds
  414. return false;
  415. }
  416. }
  417. // Generation endpoints
  418. async generateImage(params: GenerationRequest): Promise<JobInfo> {
  419. return this.request<JobInfo>("/generate/text2img", {
  420. method: "POST",
  421. body: JSON.stringify(params),
  422. });
  423. }
  424. async text2img(params: GenerationRequest): Promise<JobInfo> {
  425. return this.request<JobInfo>("/generate/text2img", {
  426. method: "POST",
  427. body: JSON.stringify(params),
  428. });
  429. }
  430. async img2img(
  431. params: GenerationRequest & { image: string },
  432. ): Promise<JobInfo> {
  433. // Convert frontend field name to backend field name
  434. const backendParams = {
  435. ...params,
  436. init_image: params.image,
  437. image: undefined, // Remove the frontend field
  438. };
  439. return this.request<JobInfo>("/generate/img2img", {
  440. method: "POST",
  441. body: JSON.stringify(backendParams),
  442. });
  443. }
  444. async inpainting(
  445. params: GenerationRequest & { source_image: string; mask_image: string },
  446. ): Promise<JobInfo> {
  447. return this.request<JobInfo>("/generate/inpainting", {
  448. method: "POST",
  449. body: JSON.stringify(params),
  450. });
  451. }
  452. async upscale(
  453. params: {
  454. image: string;
  455. model: string;
  456. upscale_factor: number;
  457. }
  458. ): Promise<JobInfo> {
  459. // Map 'model' to 'esrgan_model' as expected by the API
  460. const apiParams = {
  461. image: params.image,
  462. esrgan_model: params.model,
  463. upscale_factor: params.upscale_factor,
  464. };
  465. return this.request<JobInfo>("/generate/upscale", {
  466. method: "POST",
  467. body: JSON.stringify(apiParams),
  468. });
  469. }
  470. // Job management with caching for status checks
  471. async getJobStatus(jobId: string): Promise<JobDetailsResponse> {
  472. const cacheKey = `job_status_${jobId}`;
  473. const cachedResult = cache.get(cacheKey) as JobDetailsResponse | null;
  474. if (cachedResult) {
  475. return cachedResult;
  476. }
  477. const result = await this.request<JobDetailsResponse>(
  478. `/queue/job/${jobId}`,
  479. );
  480. // Cache job status for a short time
  481. if (result.job.status === "processing" || result.job.status === "queued") {
  482. cache.set(cacheKey, result, 2000); // Cache for 2 seconds for active jobs
  483. } else {
  484. cache.set(cacheKey, result, 10000); // Cache for 10 seconds for completed jobs
  485. }
  486. return result;
  487. }
  488. // Get authenticated image URL with cache-busting
  489. getImageUrl(jobId: string, filename: string): string {
  490. const baseUrl = this.getBaseUrl();
  491. // Add cache-busting timestamp
  492. const timestamp = Date.now();
  493. const url = `${baseUrl}/queue/job/${jobId}/output/${filename}?t=${timestamp}`;
  494. return url;
  495. }
  496. // Download image with authentication
  497. async downloadImage(jobId: string, filename: string): Promise<Blob> {
  498. const url = this.getImageUrl(jobId, filename);
  499. // Get authentication method from server config
  500. const authMethod =
  501. typeof window !== "undefined" && window.__SERVER_CONFIG__
  502. ? window.__SERVER_CONFIG__.authMethod
  503. : "jwt";
  504. // Add auth token or Unix user header based on auth method
  505. const token =
  506. typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
  507. const unixUser =
  508. typeof window !== "undefined" ? localStorage.getItem("unix_user") : null;
  509. const headers: Record<string, string> = {};
  510. if (authMethod === "unix" && unixUser) {
  511. // For Unix auth, send the username in X-Unix-User header
  512. headers["X-Unix-User"] = unixUser;
  513. } else if (token) {
  514. // For JWT auth, send the token in Authorization header
  515. headers["Authorization"] = `Bearer ${token}`;
  516. }
  517. const response = await fetch(url, {
  518. headers,
  519. });
  520. if (!response.ok) {
  521. const errorData = await response.json().catch(() => ({
  522. error: { message: response.statusText },
  523. }));
  524. // Handle nested error structure: { error: { message: "..." } }
  525. const errorMessage =
  526. errorData.error?.message ||
  527. errorData.message ||
  528. errorData.error ||
  529. "Failed to download image";
  530. throw new Error(errorMessage);
  531. }
  532. return response.blob();
  533. }
  534. // Download image from URL with server-side proxy to avoid CORS issues
  535. async downloadImageFromUrl(url: string): Promise<{
  536. mimeType: string;
  537. filename: string;
  538. base64Data: string;
  539. tempUrl?: string;
  540. tempFilename?: string;
  541. }> {
  542. const apiUrl = `${this.getBaseUrl()}/image/download?url=${encodeURIComponent(url)}`;
  543. const response = await fetch(apiUrl);
  544. if (!response.ok) {
  545. const errorData = await response.json().catch(() => ({
  546. error: { message: response.statusText },
  547. }));
  548. // Handle nested error structure: { error: { message: "..." } }
  549. const errorMessage =
  550. errorData.error?.message ||
  551. errorData.message ||
  552. errorData.error ||
  553. "Failed to download image from URL";
  554. throw new Error(errorMessage);
  555. }
  556. const result = await response.json();
  557. return {
  558. mimeType: result.mime_type,
  559. filename: result.filename,
  560. base64Data: result.base64_data,
  561. tempUrl: result.temp_url,
  562. tempFilename: result.temp_filename,
  563. };
  564. }
  565. async cancelJob(jobId: string): Promise<void> {
  566. // Clear job status cache when cancelling
  567. cache.delete(`job_status_${jobId}`);
  568. return this.request<void>("/queue/cancel", {
  569. method: "POST",
  570. body: JSON.stringify({ job_id: jobId }),
  571. });
  572. }
  573. // Get queue status with caching and throttling
  574. async getQueueStatus(): Promise<QueueStatus> {
  575. const cacheKey = "queue_status";
  576. const cachedResult = cache.get(cacheKey) as QueueStatus | null;
  577. if (cachedResult) {
  578. return cachedResult;
  579. }
  580. const response = await this.request<{ queue: QueueStatus }>(
  581. "/queue/status",
  582. );
  583. // Cache queue status based on current activity
  584. const hasActiveJobs = response.queue.jobs.some(
  585. (job) => job.status === "processing" || job.status === "queued",
  586. );
  587. // Cache for shorter time if there are active jobs
  588. const cacheTime = hasActiveJobs ? 1000 : 5000; // 1 second for active, 5 seconds for idle
  589. cache.set(cacheKey, response.queue, cacheTime);
  590. return response.queue;
  591. }
  592. async clearQueue(): Promise<void> {
  593. // Clear all related caches
  594. cache.delete("queue_status");
  595. return this.request<void>("/queue/clear", {
  596. method: "POST",
  597. });
  598. }
  599. // Model management
  600. async getModels(
  601. type?: string,
  602. loaded?: boolean,
  603. page: number = 1,
  604. limit: number = -1,
  605. search?: string,
  606. ): Promise<EnhancedModelsResponse> {
  607. const cacheKey = `models_${type || "all"}_${loaded ? "loaded" : "all"}_${page}_${limit}_${search || "all"}`;
  608. const cachedResult = cache.get(cacheKey) as EnhancedModelsResponse | null;
  609. if (cachedResult) {
  610. return cachedResult;
  611. }
  612. let endpoint = "/models";
  613. const params = [];
  614. if (type && type !== "loaded") params.push(`type=${type}`);
  615. if (type === "loaded" || loaded) params.push("loaded=true");
  616. // Only add page parameter if we're using pagination (limit > 0)
  617. if (limit > 0) {
  618. params.push(`page=${page}`);
  619. params.push(`limit=${limit}`);
  620. } else {
  621. // When limit is 0 (default), we want all models, so add limit=0 to disable pagination
  622. params.push("limit=0");
  623. }
  624. if (search) params.push(`search=${encodeURIComponent(search)}`);
  625. // Add include_metadata for additional information
  626. params.push("include_metadata=true");
  627. if (params.length > 0) endpoint += "?" + params.join("&");
  628. const response = await this.request<EnhancedModelsResponse>(endpoint);
  629. const models = response.models.map((model) => ({
  630. ...model,
  631. id: model.sha256_short || model.sha256 || model.id || model.name,
  632. size: model.file_size || model.size,
  633. path: model.path || model.name,
  634. }));
  635. const result = {
  636. ...response,
  637. models,
  638. };
  639. // Cache models for 30 seconds as they don't change frequently
  640. cache.set(cacheKey, result, 30000);
  641. return result;
  642. }
  643. // Get models with automatic selection information
  644. async getModelsForAutoSelection(
  645. checkpointModel?: string,
  646. ): Promise<EnhancedModelsResponse> {
  647. const cacheKey = `models_auto_selection_${checkpointModel || "none"}`;
  648. const cachedResult = cache.get(cacheKey) as EnhancedModelsResponse | null;
  649. if (cachedResult) {
  650. return cachedResult;
  651. }
  652. let endpoint = "/models";
  653. const params = [];
  654. params.push("include_metadata=true");
  655. params.push("include_requirements=true");
  656. if (checkpointModel) {
  657. params.push(`checkpoint=${encodeURIComponent(checkpointModel)}`);
  658. }
  659. params.push("limit=0"); // Get all models
  660. if (params.length > 0) endpoint += "?" + params.join("&");
  661. const response = await this.request<EnhancedModelsResponse>(endpoint);
  662. const models = response.models.map((model) => ({
  663. ...model,
  664. id: model.sha256_short || model.sha256 || model.id || model.name,
  665. size: model.file_size || model.size,
  666. path: model.path || model.name,
  667. }));
  668. const result = {
  669. ...response,
  670. models,
  671. };
  672. // Cache for 30 seconds
  673. cache.set(cacheKey, result, 30000);
  674. return result;
  675. }
  676. // Utility function to get models by type
  677. getModelsByType(models: ModelInfo[], type: string): ModelInfo[] {
  678. return models.filter(
  679. (model) => model.type.toLowerCase() === type.toLowerCase(),
  680. );
  681. }
  682. // Utility function to find models by name pattern
  683. findModelsByName(models: ModelInfo[], namePattern: string): ModelInfo[] {
  684. const pattern = namePattern.toLowerCase();
  685. return models.filter((model) => model.name.toLowerCase().includes(pattern));
  686. }
  687. // Utility function to get loaded models by type
  688. getLoadedModelsByType(models: ModelInfo[], type: string): ModelInfo[] {
  689. return this.getModelsByType(models, type).filter((model) => model.loaded);
  690. }
  691. // Get all models (for backward compatibility)
  692. async getAllModels(type?: string, loaded?: boolean): Promise<ModelInfo[]> {
  693. const allModels: ModelInfo[] = [];
  694. let page = 1;
  695. const limit = 100;
  696. while (true) {
  697. const response = await this.getModels(type, loaded, page, limit);
  698. allModels.push(...response.models);
  699. if (!response.pagination.has_next) {
  700. break;
  701. }
  702. page++;
  703. }
  704. return allModels;
  705. }
  706. async getModelInfo(modelId: string): Promise<ModelInfo> {
  707. const cacheKey = `model_info_${modelId}`;
  708. const cachedResult = cache.get(cacheKey) as ModelInfo | null;
  709. if (cachedResult) {
  710. return cachedResult;
  711. }
  712. const result = await this.request<ModelInfo>(`/models/${modelId}`);
  713. cache.set(cacheKey, result, 30000); // Cache for 30 seconds
  714. return result;
  715. }
  716. async loadModel(modelId: string): Promise<void> {
  717. // Clear model cache when loading
  718. cache.delete(`model_info_${modelId}`);
  719. try {
  720. return await this.request<void>(`/models/${modelId}/load`, {
  721. method: "POST",
  722. });
  723. } catch (error) {
  724. // Enhance error message for hash validation failures
  725. const errorMessage = error instanceof Error ? error.message : "Failed to load model";
  726. if (errorMessage.includes("INVALID_MODEL_IDENTIFIER") || errorMessage.includes("MODEL_NOT_FOUND")) {
  727. throw new Error(`Hash validation failed: ${errorMessage}. Please ensure you're using a valid model hash instead of a name.`);
  728. }
  729. throw error;
  730. }
  731. }
  732. async unloadModel(modelId: string): Promise<void> {
  733. // Clear model cache when unloading
  734. cache.delete(`models_*`);
  735. try {
  736. return await this.request<void>(`/models/${modelId}/unload`, {
  737. method: "POST",
  738. });
  739. } catch (error) {
  740. // Enhance error message for hash validation failures
  741. const errorMessage = error instanceof Error ? error.message : "Failed to unload model";
  742. if (errorMessage.includes("INVALID_MODEL_IDENTIFIER") || errorMessage.includes("MODEL_NOT_FOUND")) {
  743. throw new Error(`Hash validation failed: ${errorMessage}. Please ensure you're using a valid model hash instead of a name.`);
  744. }
  745. throw error;
  746. }
  747. }
  748. async computeModelHash(modelId: string): Promise<{request_id: string, status: string, message: string}> {
  749. // Clear model cache when computing hash
  750. cache.delete(`models_*`);
  751. return this.request<{request_id: string, status: string, message: string}>(`/models/hash`, {
  752. method: "POST",
  753. body: JSON.stringify({ models: [modelId] }),
  754. });
  755. }
  756. async scanModels(): Promise<void> {
  757. // Clear all model caches when scanning
  758. cache.clear();
  759. return this.request<void>("/models/refresh", {
  760. method: "POST",
  761. });
  762. }
  763. async getModelTypes(): Promise<
  764. Array<{
  765. type: string;
  766. description: string;
  767. extensions: string[];
  768. capabilities: string[];
  769. requires?: string[];
  770. recommended_for: string;
  771. }>
  772. > {
  773. const cacheKey = "model_types";
  774. const cachedResult = cache.get(cacheKey) as Array<{
  775. type: string;
  776. description: string;
  777. extensions: string[];
  778. capabilities: string[];
  779. requires?: string[];
  780. recommended_for: string;
  781. }> | null;
  782. if (cachedResult) {
  783. return cachedResult;
  784. }
  785. const response = await this.request<{
  786. model_types: Array<{
  787. type: string;
  788. description: string;
  789. extensions: string[];
  790. capabilities: string[];
  791. requires?: string[];
  792. recommended_for: string;
  793. }>;
  794. }>("/models/types");
  795. cache.set(cacheKey, response.model_types, 60000); // Cache for 1 minute
  796. return response.model_types;
  797. }
  798. async convertModel(
  799. modelName: string,
  800. quantizationType: string,
  801. outputPath?: string,
  802. ): Promise<{ request_id: string; message: string }> {
  803. return this.request<{ request_id: string; message: string }>(
  804. "/models/convert",
  805. {
  806. method: "POST",
  807. body: JSON.stringify({
  808. model_name: modelName,
  809. quantization_type: quantizationType,
  810. output_path: outputPath,
  811. }),
  812. },
  813. );
  814. }
  815. // System endpoints
  816. async getHealth(): Promise<{ status: string }> {
  817. return this.request<{ status: string }>("/health");
  818. }
  819. async getStatus(): Promise<Record<string, unknown>> {
  820. return this.request<Record<string, unknown>>("/status");
  821. }
  822. async getSystemInfo(): Promise<Record<string, unknown>> {
  823. return this.request<Record<string, unknown>>("/system");
  824. }
  825. async restartServer(): Promise<{ message: string }> {
  826. return this.request<{ message: string }>("/system/restart", {
  827. method: "POST",
  828. body: JSON.stringify({}),
  829. });
  830. }
  831. // Image manipulation endpoints
  832. async resizeImage(
  833. image: string,
  834. width: number,
  835. height: number,
  836. ): Promise<{ image: string }> {
  837. return this.request<{ image: string }>("/image/resize", {
  838. method: "POST",
  839. body: JSON.stringify({
  840. image,
  841. width,
  842. height,
  843. }),
  844. });
  845. }
  846. async cropImage(
  847. image: string,
  848. x: number,
  849. y: number,
  850. width: number,
  851. height: number,
  852. ): Promise<{ image: string }> {
  853. return this.request<{ image: string }>("/image/crop", {
  854. method: "POST",
  855. body: JSON.stringify({
  856. image,
  857. x,
  858. y,
  859. width,
  860. height,
  861. }),
  862. });
  863. }
  864. // Configuration endpoints with caching
  865. async getSamplers(): Promise<
  866. Array<{ name: string; description: string; recommended_steps: number }>
  867. > {
  868. const cacheKey = "samplers";
  869. const cachedResult = cache.get(cacheKey) as Array<{ name: string; description: string; recommended_steps: number }> | null;
  870. if (cachedResult) {
  871. return cachedResult;
  872. }
  873. const response = await this.request<{
  874. samplers: Array<{
  875. name: string;
  876. description: string;
  877. recommended_steps: number;
  878. }>;
  879. }>("/samplers");
  880. cache.set(cacheKey, response.samplers, 60000); // Cache for 1 minute
  881. return response.samplers;
  882. }
  883. async getSchedulers(): Promise<Array<{ name: string; description: string }>> {
  884. const cacheKey = "schedulers";
  885. const cachedResult = cache.get(cacheKey) as Array<{ name: string; description: string }> | null;
  886. if (cachedResult) {
  887. return cachedResult;
  888. }
  889. const response = await this.request<{
  890. schedulers: Array<{ name: string; description: string }>;
  891. }>("/schedulers");
  892. cache.set(cacheKey, response.schedulers, 60000); // Cache for 1 minute
  893. return response.schedulers;
  894. }
  895. // Cache management methods
  896. clearCache(): void {
  897. cache.clear();
  898. }
  899. clearCacheByPrefix(prefix: string): void {
  900. const keysToDelete: string[] = [];
  901. // Access the private cache property through type assertion for cleanup
  902. const cacheInstance = cache as unknown as {
  903. cache: Map<string, { data: unknown; timestamp: number; ttl: number }>
  904. };
  905. cacheInstance.cache.forEach((_: unknown, key: string) => {
  906. if (key.startsWith(prefix)) {
  907. keysToDelete.push(key);
  908. }
  909. });
  910. keysToDelete.forEach((key) => cache.delete(key));
  911. }
  912. }
  913. // Generic API request function for authentication
  914. export async function apiRequest(
  915. endpoint: string,
  916. options: RequestInit = {},
  917. ): Promise<Response> {
  918. const { apiUrl, apiBase } = getApiConfig();
  919. const url = `${apiUrl}${apiBase}${endpoint}`;
  920. // For both Unix and JWT auth, send username and password
  921. // The server will handle whether password is required based on PAM availability
  922. // Get authentication method from server config
  923. const authMethod =
  924. typeof window !== "undefined" && window.__SERVER_CONFIG__
  925. ? window.__SERVER_CONFIG__.authMethod
  926. : "jwt";
  927. // Add auth token or Unix user header based on auth method
  928. const token =
  929. typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
  930. const unixUser =
  931. typeof window !== "undefined" ? localStorage.getItem("unix_user") : null;
  932. const headers: Record<string, string> = {
  933. "Content-Type": "application/json",
  934. ...(options.headers as Record<string, string>),
  935. };
  936. if (authMethod === "unix" && unixUser) {
  937. // For Unix auth, send the username in X-Unix-User header
  938. headers["X-Unix-User"] = unixUser;
  939. } else if (token) {
  940. // For JWT auth, send the token in Authorization header
  941. headers["Authorization"] = `Bearer ${token}`;
  942. }
  943. return fetch(url, {
  944. ...options,
  945. headers,
  946. });
  947. }
  948. // Authentication API endpoints
  949. export const authApi = {
  950. async login(username: string, password?: string) {
  951. // Get authentication method from server config
  952. const authMethod =
  953. typeof window !== "undefined" && window.__SERVER_CONFIG__
  954. ? window.__SERVER_CONFIG__.authMethod
  955. : "jwt";
  956. // For both Unix and JWT auth, send username and password
  957. // The server will handle whether password is required based on PAM availability
  958. const response = await apiRequest("/auth/login", {
  959. method: "POST",
  960. body: JSON.stringify({ username, password }),
  961. });
  962. if (!response.ok) {
  963. const error = await response
  964. .json()
  965. .catch(() => ({ message: "Login failed" }));
  966. throw new Error(error.message || "Login failed");
  967. }
  968. return response.json();
  969. },
  970. async validateToken(token: string) {
  971. const response = await apiRequest("/auth/validate", {
  972. headers: { Authorization: `Bearer ${token}` },
  973. });
  974. if (!response.ok) {
  975. throw new Error("Token validation failed");
  976. }
  977. return response.json();
  978. },
  979. async refreshToken() {
  980. const response = await apiRequest("/auth/refresh", {
  981. method: "POST",
  982. });
  983. if (!response.ok) {
  984. throw new Error("Token refresh failed");
  985. }
  986. return response.json();
  987. },
  988. async logout() {
  989. await apiRequest("/auth/logout", {
  990. method: "POST",
  991. });
  992. },
  993. async getCurrentUser() {
  994. const response = await apiRequest("/auth/me");
  995. if (!response.ok) {
  996. throw new Error("Failed to get current user");
  997. }
  998. return response.json();
  999. },
  1000. async changePassword(oldPassword: string, newPassword: string) {
  1001. const response = await apiRequest("/auth/change-password", {
  1002. method: "POST",
  1003. body: JSON.stringify({
  1004. old_password: oldPassword,
  1005. new_password: newPassword,
  1006. }),
  1007. });
  1008. if (!response.ok) {
  1009. const error = await response
  1010. .json()
  1011. .catch(() => ({ message: "Password change failed" }));
  1012. throw new Error(error.message || "Password change failed");
  1013. }
  1014. return response.json();
  1015. },
  1016. // API Key management
  1017. async getApiKeys() {
  1018. const response = await apiRequest("/auth/api-keys");
  1019. if (!response.ok) {
  1020. throw new Error("Failed to get API keys");
  1021. }
  1022. return response.json();
  1023. },
  1024. async createApiKey(name: string, scopes?: string[]) {
  1025. const response = await apiRequest("/auth/api-keys", {
  1026. method: "POST",
  1027. body: JSON.stringify({ name, scopes }),
  1028. });
  1029. if (!response.ok) {
  1030. const error = await response
  1031. .json()
  1032. .catch(() => ({ message: "Failed to create API key" }));
  1033. throw new Error(error.message || "Failed to create API key");
  1034. }
  1035. return response.json();
  1036. },
  1037. async deleteApiKey(keyId: string) {
  1038. const response = await apiRequest(`/auth/api-keys/${keyId}`, {
  1039. method: "DELETE",
  1040. });
  1041. if (!response.ok) {
  1042. throw new Error("Failed to delete API key");
  1043. }
  1044. return response.json();
  1045. },
  1046. // User management (admin only)
  1047. async getUsers() {
  1048. const response = await apiRequest("/auth/users");
  1049. if (!response.ok) {
  1050. throw new Error("Failed to get users");
  1051. }
  1052. return response.json();
  1053. },
  1054. async createUser(userData: {
  1055. username: string;
  1056. email?: string;
  1057. password: string;
  1058. role?: string;
  1059. }) {
  1060. const response = await apiRequest("/auth/users", {
  1061. method: "POST",
  1062. body: JSON.stringify(userData),
  1063. });
  1064. if (!response.ok) {
  1065. const error = await response
  1066. .json()
  1067. .catch(() => ({ message: "Failed to create user" }));
  1068. throw new Error(error.message || "Failed to create user");
  1069. }
  1070. return response.json();
  1071. },
  1072. async updateUser(
  1073. userId: string,
  1074. userData: { email?: string; role?: string; active?: boolean },
  1075. ) {
  1076. const response = await apiRequest(`/auth/users/${userId}`, {
  1077. method: "PUT",
  1078. body: JSON.stringify(userData),
  1079. });
  1080. if (!response.ok) {
  1081. const error = await response
  1082. .json()
  1083. .catch(() => ({ message: "Failed to update user" }));
  1084. throw new Error(error.message || "Failed to update user");
  1085. }
  1086. return response.json();
  1087. },
  1088. async deleteUser(userId: string) {
  1089. const response = await apiRequest(`/auth/users/${userId}`, {
  1090. method: "DELETE",
  1091. });
  1092. if (!response.ok) {
  1093. throw new Error("Failed to delete user");
  1094. }
  1095. return response.json();
  1096. },
  1097. };
  1098. // Version API
  1099. export async function getVersion(): Promise<VersionInfo> {
  1100. const response = await apiRequest("/version");
  1101. if (!response.ok) {
  1102. throw new Error("Failed to get version information");
  1103. }
  1104. return response.json();
  1105. }
  1106. export const apiClient = new ApiClient();