api.ts 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260
  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. // Job management with caching for status checks
  450. async getJobStatus(jobId: string): Promise<JobDetailsResponse> {
  451. const cacheKey = `job_status_${jobId}`;
  452. const cachedResult = cache.get(cacheKey) as JobDetailsResponse | null;
  453. if (cachedResult) {
  454. return cachedResult;
  455. }
  456. const result = await this.request<JobDetailsResponse>(
  457. `/queue/job/${jobId}`,
  458. );
  459. // Cache job status for a short time
  460. if (result.job.status === "processing" || result.job.status === "queued") {
  461. cache.set(cacheKey, result, 2000); // Cache for 2 seconds for active jobs
  462. } else {
  463. cache.set(cacheKey, result, 10000); // Cache for 10 seconds for completed jobs
  464. }
  465. return result;
  466. }
  467. // Get authenticated image URL with cache-busting
  468. getImageUrl(jobId: string, filename: string): string {
  469. const baseUrl = this.getBaseUrl();
  470. // Add cache-busting timestamp
  471. const timestamp = Date.now();
  472. const url = `${baseUrl}/queue/job/${jobId}/output/${filename}?t=${timestamp}`;
  473. return url;
  474. }
  475. // Download image with authentication
  476. async downloadImage(jobId: string, filename: string): Promise<Blob> {
  477. const url = this.getImageUrl(jobId, filename);
  478. // Get authentication method from server config
  479. const authMethod =
  480. typeof window !== "undefined" && window.__SERVER_CONFIG__
  481. ? window.__SERVER_CONFIG__.authMethod
  482. : "jwt";
  483. // Add auth token or Unix user header based on auth method
  484. const token =
  485. typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
  486. const unixUser =
  487. typeof window !== "undefined" ? localStorage.getItem("unix_user") : null;
  488. const headers: Record<string, string> = {};
  489. if (authMethod === "unix" && unixUser) {
  490. // For Unix auth, send the username in X-Unix-User header
  491. headers["X-Unix-User"] = unixUser;
  492. } else if (token) {
  493. // For JWT auth, send the token in Authorization header
  494. headers["Authorization"] = `Bearer ${token}`;
  495. }
  496. const response = await fetch(url, {
  497. headers,
  498. });
  499. if (!response.ok) {
  500. const errorData = await response.json().catch(() => ({
  501. error: { message: response.statusText },
  502. }));
  503. // Handle nested error structure: { error: { message: "..." } }
  504. const errorMessage =
  505. errorData.error?.message ||
  506. errorData.message ||
  507. errorData.error ||
  508. "Failed to download image";
  509. throw new Error(errorMessage);
  510. }
  511. return response.blob();
  512. }
  513. // Download image from URL with server-side proxy to avoid CORS issues
  514. async downloadImageFromUrl(url: string): Promise<{
  515. mimeType: string;
  516. filename: string;
  517. base64Data: string;
  518. tempUrl?: string;
  519. tempFilename?: string;
  520. }> {
  521. const apiUrl = `${this.getBaseUrl()}/image/download?url=${encodeURIComponent(url)}`;
  522. const response = await fetch(apiUrl);
  523. if (!response.ok) {
  524. const errorData = await response.json().catch(() => ({
  525. error: { message: response.statusText },
  526. }));
  527. // Handle nested error structure: { error: { message: "..." } }
  528. const errorMessage =
  529. errorData.error?.message ||
  530. errorData.message ||
  531. errorData.error ||
  532. "Failed to download image from URL";
  533. throw new Error(errorMessage);
  534. }
  535. const result = await response.json();
  536. return {
  537. mimeType: result.mime_type,
  538. filename: result.filename,
  539. base64Data: result.base64_data,
  540. tempUrl: result.temp_url,
  541. tempFilename: result.temp_filename,
  542. };
  543. }
  544. async cancelJob(jobId: string): Promise<void> {
  545. // Clear job status cache when cancelling
  546. cache.delete(`job_status_${jobId}`);
  547. return this.request<void>("/queue/cancel", {
  548. method: "POST",
  549. body: JSON.stringify({ job_id: jobId }),
  550. });
  551. }
  552. // Get queue status with caching and throttling
  553. async getQueueStatus(): Promise<QueueStatus> {
  554. const cacheKey = "queue_status";
  555. const cachedResult = cache.get(cacheKey) as QueueStatus | null;
  556. if (cachedResult) {
  557. return cachedResult;
  558. }
  559. const response = await this.request<{ queue: QueueStatus }>(
  560. "/queue/status",
  561. );
  562. // Cache queue status based on current activity
  563. const hasActiveJobs = response.queue.jobs.some(
  564. (job) => job.status === "processing" || job.status === "queued",
  565. );
  566. // Cache for shorter time if there are active jobs
  567. const cacheTime = hasActiveJobs ? 1000 : 5000; // 1 second for active, 5 seconds for idle
  568. cache.set(cacheKey, response.queue, cacheTime);
  569. return response.queue;
  570. }
  571. async clearQueue(): Promise<void> {
  572. // Clear all related caches
  573. cache.delete("queue_status");
  574. return this.request<void>("/queue/clear", {
  575. method: "POST",
  576. });
  577. }
  578. // Model management
  579. async getModels(
  580. type?: string,
  581. loaded?: boolean,
  582. page: number = 1,
  583. limit: number = -1,
  584. search?: string,
  585. ): Promise<EnhancedModelsResponse> {
  586. const cacheKey = `models_${type || "all"}_${loaded ? "loaded" : "all"}_${page}_${limit}_${search || "all"}`;
  587. const cachedResult = cache.get(cacheKey) as EnhancedModelsResponse | null;
  588. if (cachedResult) {
  589. return cachedResult;
  590. }
  591. let endpoint = "/models";
  592. const params = [];
  593. if (type && type !== "loaded") params.push(`type=${type}`);
  594. if (type === "loaded" || loaded) params.push("loaded=true");
  595. // Only add page parameter if we're using pagination (limit > 0)
  596. if (limit > 0) {
  597. params.push(`page=${page}`);
  598. params.push(`limit=${limit}`);
  599. } else {
  600. // When limit is 0 (default), we want all models, so add limit=0 to disable pagination
  601. params.push("limit=0");
  602. }
  603. if (search) params.push(`search=${encodeURIComponent(search)}`);
  604. // Add include_metadata for additional information
  605. params.push("include_metadata=true");
  606. if (params.length > 0) endpoint += "?" + params.join("&");
  607. const response = await this.request<EnhancedModelsResponse>(endpoint);
  608. const models = response.models.map((model) => ({
  609. ...model,
  610. id: model.sha256_short || model.name,
  611. size: model.file_size || model.size,
  612. path: model.path || model.name,
  613. }));
  614. const result = {
  615. ...response,
  616. models,
  617. };
  618. // Cache models for 30 seconds as they don't change frequently
  619. cache.set(cacheKey, result, 30000);
  620. return result;
  621. }
  622. // Get models with automatic selection information
  623. async getModelsForAutoSelection(
  624. checkpointModel?: string,
  625. ): Promise<EnhancedModelsResponse> {
  626. const cacheKey = `models_auto_selection_${checkpointModel || "none"}`;
  627. const cachedResult = cache.get(cacheKey) as EnhancedModelsResponse | null;
  628. if (cachedResult) {
  629. return cachedResult;
  630. }
  631. let endpoint = "/models";
  632. const params = [];
  633. params.push("include_metadata=true");
  634. params.push("include_requirements=true");
  635. if (checkpointModel) {
  636. params.push(`checkpoint=${encodeURIComponent(checkpointModel)}`);
  637. }
  638. params.push("limit=0"); // Get all models
  639. if (params.length > 0) endpoint += "?" + params.join("&");
  640. const response = await this.request<EnhancedModelsResponse>(endpoint);
  641. const models = response.models.map((model) => ({
  642. ...model,
  643. id: model.sha256_short || model.name,
  644. size: model.file_size || model.size,
  645. path: model.path || model.name,
  646. }));
  647. const result = {
  648. ...response,
  649. models,
  650. };
  651. // Cache for 30 seconds
  652. cache.set(cacheKey, result, 30000);
  653. return result;
  654. }
  655. // Utility function to get models by type
  656. getModelsByType(models: ModelInfo[], type: string): ModelInfo[] {
  657. return models.filter(
  658. (model) => model.type.toLowerCase() === type.toLowerCase(),
  659. );
  660. }
  661. // Utility function to find models by name pattern
  662. findModelsByName(models: ModelInfo[], namePattern: string): ModelInfo[] {
  663. const pattern = namePattern.toLowerCase();
  664. return models.filter((model) => model.name.toLowerCase().includes(pattern));
  665. }
  666. // Utility function to get loaded models by type
  667. getLoadedModelsByType(models: ModelInfo[], type: string): ModelInfo[] {
  668. return this.getModelsByType(models, type).filter((model) => model.loaded);
  669. }
  670. // Get all models (for backward compatibility)
  671. async getAllModels(type?: string, loaded?: boolean): Promise<ModelInfo[]> {
  672. const allModels: ModelInfo[] = [];
  673. let page = 1;
  674. const limit = 100;
  675. while (true) {
  676. const response = await this.getModels(type, loaded, page, limit);
  677. allModels.push(...response.models);
  678. if (!response.pagination.has_next) {
  679. break;
  680. }
  681. page++;
  682. }
  683. return allModels;
  684. }
  685. async getModelInfo(modelId: string): Promise<ModelInfo> {
  686. const cacheKey = `model_info_${modelId}`;
  687. const cachedResult = cache.get(cacheKey) as ModelInfo | null;
  688. if (cachedResult) {
  689. return cachedResult;
  690. }
  691. const result = await this.request<ModelInfo>(`/models/${modelId}`);
  692. cache.set(cacheKey, result, 30000); // Cache for 30 seconds
  693. return result;
  694. }
  695. async loadModel(modelId: string): Promise<void> {
  696. // Clear model cache when loading
  697. cache.delete(`model_info_${modelId}`);
  698. return this.request<void>(`/models/${modelId}/load`, {
  699. method: "POST",
  700. });
  701. }
  702. async unloadModel(modelId: string): Promise<void> {
  703. // Clear model cache when unloading
  704. cache.delete(`model_info_${modelId}`);
  705. return this.request<void>(`/models/${modelId}/unload`, {
  706. method: "POST",
  707. });
  708. }
  709. async scanModels(): Promise<void> {
  710. // Clear all model caches when scanning
  711. cache.clear();
  712. return this.request<void>("/models/refresh", {
  713. method: "POST",
  714. });
  715. }
  716. async getModelTypes(): Promise<
  717. Array<{
  718. type: string;
  719. description: string;
  720. extensions: string[];
  721. capabilities: string[];
  722. requires?: string[];
  723. recommended_for: string;
  724. }>
  725. > {
  726. const cacheKey = "model_types";
  727. const cachedResult = cache.get(cacheKey) as Array<{
  728. type: string;
  729. description: string;
  730. extensions: string[];
  731. capabilities: string[];
  732. requires?: string[];
  733. recommended_for: string;
  734. }> | null;
  735. if (cachedResult) {
  736. return cachedResult;
  737. }
  738. const response = await this.request<{
  739. model_types: Array<{
  740. type: string;
  741. description: string;
  742. extensions: string[];
  743. capabilities: string[];
  744. requires?: string[];
  745. recommended_for: string;
  746. }>;
  747. }>("/models/types");
  748. cache.set(cacheKey, response.model_types, 60000); // Cache for 1 minute
  749. return response.model_types;
  750. }
  751. async convertModel(
  752. modelName: string,
  753. quantizationType: string,
  754. outputPath?: string,
  755. ): Promise<{ request_id: string; message: string }> {
  756. return this.request<{ request_id: string; message: string }>(
  757. "/models/convert",
  758. {
  759. method: "POST",
  760. body: JSON.stringify({
  761. model_name: modelName,
  762. quantization_type: quantizationType,
  763. output_path: outputPath,
  764. }),
  765. },
  766. );
  767. }
  768. // System endpoints
  769. async getHealth(): Promise<{ status: string }> {
  770. return this.request<{ status: string }>("/health");
  771. }
  772. async getStatus(): Promise<Record<string, unknown>> {
  773. return this.request<Record<string, unknown>>("/status");
  774. }
  775. async getSystemInfo(): Promise<Record<string, unknown>> {
  776. return this.request<Record<string, unknown>>("/system");
  777. }
  778. async restartServer(): Promise<{ message: string }> {
  779. return this.request<{ message: string }>("/system/restart", {
  780. method: "POST",
  781. body: JSON.stringify({}),
  782. });
  783. }
  784. // Image manipulation endpoints
  785. async resizeImage(
  786. image: string,
  787. width: number,
  788. height: number,
  789. ): Promise<{ image: string }> {
  790. return this.request<{ image: string }>("/image/resize", {
  791. method: "POST",
  792. body: JSON.stringify({
  793. image,
  794. width,
  795. height,
  796. }),
  797. });
  798. }
  799. async cropImage(
  800. image: string,
  801. x: number,
  802. y: number,
  803. width: number,
  804. height: number,
  805. ): Promise<{ image: string }> {
  806. return this.request<{ image: string }>("/image/crop", {
  807. method: "POST",
  808. body: JSON.stringify({
  809. image,
  810. x,
  811. y,
  812. width,
  813. height,
  814. }),
  815. });
  816. }
  817. // Configuration endpoints with caching
  818. async getSamplers(): Promise<
  819. Array<{ name: string; description: string; recommended_steps: number }>
  820. > {
  821. const cacheKey = "samplers";
  822. const cachedResult = cache.get(cacheKey) as Array<{ name: string; description: string; recommended_steps: number }> | null;
  823. if (cachedResult) {
  824. return cachedResult;
  825. }
  826. const response = await this.request<{
  827. samplers: Array<{
  828. name: string;
  829. description: string;
  830. recommended_steps: number;
  831. }>;
  832. }>("/samplers");
  833. cache.set(cacheKey, response.samplers, 60000); // Cache for 1 minute
  834. return response.samplers;
  835. }
  836. async getSchedulers(): Promise<Array<{ name: string; description: string }>> {
  837. const cacheKey = "schedulers";
  838. const cachedResult = cache.get(cacheKey) as Array<{ name: string; description: string }> | null;
  839. if (cachedResult) {
  840. return cachedResult;
  841. }
  842. const response = await this.request<{
  843. schedulers: Array<{ name: string; description: string }>;
  844. }>("/schedulers");
  845. cache.set(cacheKey, response.schedulers, 60000); // Cache for 1 minute
  846. return response.schedulers;
  847. }
  848. // Cache management methods
  849. clearCache(): void {
  850. cache.clear();
  851. }
  852. clearCacheByPrefix(prefix: string): void {
  853. const keysToDelete: string[] = [];
  854. // Access the private cache property through type assertion for cleanup
  855. const cacheInstance = cache as unknown as {
  856. cache: Map<string, { data: unknown; timestamp: number; ttl: number }>
  857. };
  858. cacheInstance.cache.forEach((_: unknown, key: string) => {
  859. if (key.startsWith(prefix)) {
  860. keysToDelete.push(key);
  861. }
  862. });
  863. keysToDelete.forEach((key) => cache.delete(key));
  864. }
  865. }
  866. // Generic API request function for authentication
  867. export async function apiRequest(
  868. endpoint: string,
  869. options: RequestInit = {},
  870. ): Promise<Response> {
  871. const { apiUrl, apiBase } = getApiConfig();
  872. const url = `${apiUrl}${apiBase}${endpoint}`;
  873. // For both Unix and JWT auth, send username and password
  874. // The server will handle whether password is required based on PAM availability
  875. // Get authentication method from server config
  876. const authMethod =
  877. typeof window !== "undefined" && window.__SERVER_CONFIG__
  878. ? window.__SERVER_CONFIG__.authMethod
  879. : "jwt";
  880. // Add auth token or Unix user header based on auth method
  881. const token =
  882. typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
  883. const unixUser =
  884. typeof window !== "undefined" ? localStorage.getItem("unix_user") : null;
  885. const headers: Record<string, string> = {
  886. "Content-Type": "application/json",
  887. ...(options.headers as Record<string, string>),
  888. };
  889. if (authMethod === "unix" && unixUser) {
  890. // For Unix auth, send the username in X-Unix-User header
  891. headers["X-Unix-User"] = unixUser;
  892. } else if (token) {
  893. // For JWT auth, send the token in Authorization header
  894. headers["Authorization"] = `Bearer ${token}`;
  895. }
  896. return fetch(url, {
  897. ...options,
  898. headers,
  899. });
  900. }
  901. // Authentication API endpoints
  902. export const authApi = {
  903. async login(username: string, password?: string) {
  904. // Get authentication method from server config
  905. const authMethod =
  906. typeof window !== "undefined" && window.__SERVER_CONFIG__
  907. ? window.__SERVER_CONFIG__.authMethod
  908. : "jwt";
  909. // For both Unix and JWT auth, send username and password
  910. // The server will handle whether password is required based on PAM availability
  911. const response = await apiRequest("/auth/login", {
  912. method: "POST",
  913. body: JSON.stringify({ username, password }),
  914. });
  915. if (!response.ok) {
  916. const error = await response
  917. .json()
  918. .catch(() => ({ message: "Login failed" }));
  919. throw new Error(error.message || "Login failed");
  920. }
  921. return response.json();
  922. },
  923. async validateToken(token: string) {
  924. const response = await apiRequest("/auth/validate", {
  925. headers: { Authorization: `Bearer ${token}` },
  926. });
  927. if (!response.ok) {
  928. throw new Error("Token validation failed");
  929. }
  930. return response.json();
  931. },
  932. async refreshToken() {
  933. const response = await apiRequest("/auth/refresh", {
  934. method: "POST",
  935. });
  936. if (!response.ok) {
  937. throw new Error("Token refresh failed");
  938. }
  939. return response.json();
  940. },
  941. async logout() {
  942. await apiRequest("/auth/logout", {
  943. method: "POST",
  944. });
  945. },
  946. async getCurrentUser() {
  947. const response = await apiRequest("/auth/me");
  948. if (!response.ok) {
  949. throw new Error("Failed to get current user");
  950. }
  951. return response.json();
  952. },
  953. async changePassword(oldPassword: string, newPassword: string) {
  954. const response = await apiRequest("/auth/change-password", {
  955. method: "POST",
  956. body: JSON.stringify({
  957. old_password: oldPassword,
  958. new_password: newPassword,
  959. }),
  960. });
  961. if (!response.ok) {
  962. const error = await response
  963. .json()
  964. .catch(() => ({ message: "Password change failed" }));
  965. throw new Error(error.message || "Password change failed");
  966. }
  967. return response.json();
  968. },
  969. // API Key management
  970. async getApiKeys() {
  971. const response = await apiRequest("/auth/api-keys");
  972. if (!response.ok) {
  973. throw new Error("Failed to get API keys");
  974. }
  975. return response.json();
  976. },
  977. async createApiKey(name: string, scopes?: string[]) {
  978. const response = await apiRequest("/auth/api-keys", {
  979. method: "POST",
  980. body: JSON.stringify({ name, scopes }),
  981. });
  982. if (!response.ok) {
  983. const error = await response
  984. .json()
  985. .catch(() => ({ message: "Failed to create API key" }));
  986. throw new Error(error.message || "Failed to create API key");
  987. }
  988. return response.json();
  989. },
  990. async deleteApiKey(keyId: string) {
  991. const response = await apiRequest(`/auth/api-keys/${keyId}`, {
  992. method: "DELETE",
  993. });
  994. if (!response.ok) {
  995. throw new Error("Failed to delete API key");
  996. }
  997. return response.json();
  998. },
  999. // User management (admin only)
  1000. async getUsers() {
  1001. const response = await apiRequest("/auth/users");
  1002. if (!response.ok) {
  1003. throw new Error("Failed to get users");
  1004. }
  1005. return response.json();
  1006. },
  1007. async createUser(userData: {
  1008. username: string;
  1009. email?: string;
  1010. password: string;
  1011. role?: string;
  1012. }) {
  1013. const response = await apiRequest("/auth/users", {
  1014. method: "POST",
  1015. body: JSON.stringify(userData),
  1016. });
  1017. if (!response.ok) {
  1018. const error = await response
  1019. .json()
  1020. .catch(() => ({ message: "Failed to create user" }));
  1021. throw new Error(error.message || "Failed to create user");
  1022. }
  1023. return response.json();
  1024. },
  1025. async updateUser(
  1026. userId: string,
  1027. userData: { email?: string; role?: string; active?: boolean },
  1028. ) {
  1029. const response = await apiRequest(`/auth/users/${userId}`, {
  1030. method: "PUT",
  1031. body: JSON.stringify(userData),
  1032. });
  1033. if (!response.ok) {
  1034. const error = await response
  1035. .json()
  1036. .catch(() => ({ message: "Failed to update user" }));
  1037. throw new Error(error.message || "Failed to update user");
  1038. }
  1039. return response.json();
  1040. },
  1041. async deleteUser(userId: string) {
  1042. const response = await apiRequest(`/auth/users/${userId}`, {
  1043. method: "DELETE",
  1044. });
  1045. if (!response.ok) {
  1046. throw new Error("Failed to delete user");
  1047. }
  1048. return response.json();
  1049. },
  1050. };
  1051. // Version API
  1052. export async function getVersion(): Promise<VersionInfo> {
  1053. const response = await apiRequest("/version");
  1054. if (!response.ok) {
  1055. throw new Error("Failed to get version information");
  1056. }
  1057. return response.json();
  1058. }
  1059. export const apiClient = new ApiClient();