api.ts 36 KB

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