api.ts 34 KB

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