api.ts 31 KB

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