api.ts 34 KB

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