api.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092
  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. class ApiClient {
  215. private baseUrl: string = '';
  216. private isInitialized: boolean = false;
  217. // Initialize base URL
  218. private initBaseUrl(): string {
  219. if (!this.isInitialized) {
  220. const config = getApiConfig();
  221. this.baseUrl = `${config.apiUrl}${config.apiBase}`;
  222. this.isInitialized = true;
  223. }
  224. return this.baseUrl;
  225. }
  226. // Get base URL dynamically at runtime to ensure server config is loaded
  227. private getBaseUrl(): string {
  228. return this.initBaseUrl();
  229. }
  230. private async request<T>(
  231. endpoint: string,
  232. options: RequestInit = {}
  233. ): Promise<T> {
  234. const url = `${this.getBaseUrl()}${endpoint}`;
  235. // Check request throttling for certain endpoints
  236. const needsThrottling = endpoint.includes('/queue/status') || endpoint.includes('/health');
  237. if (needsThrottling) {
  238. const waitTime = throttler.getWaitTime(endpoint);
  239. if (waitTime > 0) {
  240. // Wait before making the request
  241. await new Promise(resolve => setTimeout(resolve, waitTime));
  242. }
  243. if (!throttler.canMakeRequest(endpoint)) {
  244. throw new Error('Too many requests. Please wait before making another request.');
  245. }
  246. }
  247. // Get authentication method from server config
  248. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  249. ? window.__SERVER_CONFIG__.authMethod
  250. : 'jwt';
  251. // Add auth token or Unix user header based on auth method
  252. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  253. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  254. const headers: Record<string, string> = {
  255. 'Content-Type': 'application/json',
  256. ...options.headers as Record<string, string>,
  257. };
  258. if (authMethod === 'unix' && unixUser) {
  259. // For Unix auth, send the username in X-Unix-User header
  260. headers['X-Unix-User'] = unixUser;
  261. } else if (token) {
  262. // For JWT auth, send the token in Authorization header
  263. headers['Authorization'] = `Bearer ${token}`;
  264. }
  265. const response = await fetch(url, {
  266. ...options,
  267. headers,
  268. });
  269. if (!response.ok) {
  270. const errorData = await response.json().catch(() => ({
  271. error: { message: response.statusText },
  272. }));
  273. // Handle nested error structure: { error: { message: "..." } }
  274. const errorMessage =
  275. errorData.error?.message ||
  276. errorData.message ||
  277. errorData.error ||
  278. 'API request failed';
  279. throw new Error(errorMessage);
  280. }
  281. return response.json();
  282. }
  283. // Enhanced health check with caching and better error handling
  284. async checkHealth(): Promise<HealthStatus> {
  285. const cacheKey = 'health_check';
  286. const cachedResult = cache.get(cacheKey);
  287. if (cachedResult) {
  288. return cachedResult;
  289. }
  290. const endpoints = ['/queue/status', '/health', '/status', '/'];
  291. for (const endpoint of endpoints) {
  292. try {
  293. const response = await fetch(`${this.getBaseUrl()}${endpoint}`, {
  294. method: 'GET',
  295. headers: {
  296. 'Content-Type': 'application/json',
  297. },
  298. // Add timeout to prevent hanging requests
  299. signal: AbortSignal.timeout(3000), // Reduced timeout
  300. });
  301. if (response.ok) {
  302. const data = await response.json();
  303. // For queue status, consider it healthy if it returns valid structure
  304. if (endpoint === '/queue/status' && data.queue) {
  305. const result = {
  306. status: 'ok' as const,
  307. message: 'API is running and queue is accessible',
  308. timestamp: new Date().toISOString(),
  309. };
  310. cache.set(cacheKey, result, 10000); // Cache for 10 seconds
  311. return result;
  312. }
  313. // For other health endpoints
  314. const healthStatus: HealthStatus = {
  315. status: 'ok',
  316. message: 'API is running',
  317. timestamp: new Date().toISOString(),
  318. uptime: data.uptime,
  319. version: data.version || data.build || data.git_version,
  320. };
  321. // Handle different response formats
  322. if (data.status) {
  323. if (data.status === 'healthy' || data.status === 'running' || data.status === 'ok') {
  324. healthStatus.status = 'ok';
  325. healthStatus.message = data.message || 'API is running';
  326. } else if (data.status === 'degraded') {
  327. healthStatus.status = 'degraded';
  328. healthStatus.message = data.message || 'API is running in degraded mode';
  329. } else {
  330. healthStatus.status = 'error';
  331. healthStatus.message = data.message || 'API status is unknown';
  332. }
  333. }
  334. cache.set(cacheKey, healthStatus, 10000); // Cache for 10 seconds
  335. return healthStatus;
  336. }
  337. } catch (error) {
  338. // Continue to next endpoint if this one fails
  339. console.warn(`Health check failed for endpoint ${endpoint}:`, error);
  340. continue;
  341. }
  342. }
  343. // If all endpoints fail
  344. throw new Error('All health check endpoints are unavailable');
  345. }
  346. // Alternative simple connectivity check with caching
  347. async checkConnectivity(): Promise<boolean> {
  348. const cacheKey = 'connectivity_check';
  349. const cachedResult = cache.get(cacheKey);
  350. if (cachedResult !== null) {
  351. return cachedResult;
  352. }
  353. try {
  354. const response = await fetch(`${this.getBaseUrl()}`, {
  355. method: 'HEAD',
  356. signal: AbortSignal.timeout(2000), // Reduced timeout
  357. });
  358. const result = response.ok || response.status < 500;
  359. cache.set(cacheKey, result, 5000); // Cache for 5 seconds
  360. return result;
  361. } catch (error) {
  362. cache.set(cacheKey, false, 5000); // Cache failure for 5 seconds
  363. return false;
  364. }
  365. }
  366. // Generation endpoints
  367. async generateImage(params: GenerationRequest): Promise<JobInfo> {
  368. return this.request<JobInfo>('/generate/text2img', {
  369. method: 'POST',
  370. body: JSON.stringify(params),
  371. });
  372. }
  373. async text2img(params: GenerationRequest): Promise<JobInfo> {
  374. return this.request<JobInfo>('/generate/text2img', {
  375. method: 'POST',
  376. body: JSON.stringify(params),
  377. });
  378. }
  379. async img2img(params: GenerationRequest & { image: string }): Promise<JobInfo> {
  380. // Convert frontend field name to backend field name
  381. const backendParams = {
  382. ...params,
  383. init_image: params.image,
  384. image: undefined, // Remove the frontend field
  385. };
  386. return this.request<JobInfo>('/generate/img2img', {
  387. method: 'POST',
  388. body: JSON.stringify(backendParams),
  389. });
  390. }
  391. async inpainting(params: GenerationRequest & { source_image: string; mask_image: string }): Promise<JobInfo> {
  392. return this.request<JobInfo>('/generate/inpainting', {
  393. method: 'POST',
  394. body: JSON.stringify(params),
  395. });
  396. }
  397. // Job management with caching for status checks
  398. async getJobStatus(jobId: string): Promise<JobInfo> {
  399. const cacheKey = `job_status_${jobId}`;
  400. const cachedResult = cache.get(cacheKey);
  401. if (cachedResult) {
  402. return cachedResult;
  403. }
  404. const result = await this.request<JobInfo>(`/queue/job/${jobId}`);
  405. // Cache job status for a short time
  406. if (result.status === 'processing' || result.status === 'queued') {
  407. cache.set(cacheKey, result, 2000); // Cache for 2 seconds for active jobs
  408. } else {
  409. cache.set(cacheKey, result, 10000); // Cache for 10 seconds for completed jobs
  410. }
  411. return result;
  412. }
  413. // Get authenticated image URL with cache-busting
  414. getImageUrl(jobId: string, filename: string): string {
  415. const baseUrl = this.getBaseUrl();
  416. // Add cache-busting timestamp
  417. const timestamp = Date.now();
  418. const url = `${baseUrl}/queue/job/${jobId}/output/${filename}?t=${timestamp}`;
  419. return url;
  420. }
  421. // Download image with authentication
  422. async downloadImage(jobId: string, filename: string): Promise<Blob> {
  423. const url = this.getImageUrl(jobId, filename);
  424. // Get authentication method from server config
  425. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  426. ? window.__SERVER_CONFIG__.authMethod
  427. : 'jwt';
  428. // Add auth token or Unix user header based on auth method
  429. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  430. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  431. const headers: Record<string, string> = {};
  432. if (authMethod === 'unix' && unixUser) {
  433. // For Unix auth, send the username in X-Unix-User header
  434. headers['X-Unix-User'] = unixUser;
  435. } else if (token) {
  436. // For JWT auth, send the token in Authorization header
  437. headers['Authorization'] = `Bearer ${token}`;
  438. }
  439. const response = await fetch(url, {
  440. headers,
  441. });
  442. if (!response.ok) {
  443. const errorData = await response.json().catch(() => ({
  444. error: { message: response.statusText },
  445. }));
  446. // Handle nested error structure: { error: { message: "..." } }
  447. const errorMessage =
  448. errorData.error?.message ||
  449. errorData.message ||
  450. errorData.error ||
  451. 'Failed to download image';
  452. throw new Error(errorMessage);
  453. }
  454. return response.blob();
  455. }
  456. // Download image from URL with server-side proxy to avoid CORS issues
  457. async downloadImageFromUrl(url: string): Promise<{
  458. mimeType: string;
  459. filename: string;
  460. base64Data: string;
  461. }> {
  462. const apiUrl = `${this.getBaseUrl()}/image/download?url=${encodeURIComponent(url)}`;
  463. const response = await fetch(apiUrl);
  464. if (!response.ok) {
  465. const errorData = await response.json().catch(() => ({
  466. error: { message: response.statusText },
  467. }));
  468. // Handle nested error structure: { error: { message: "..." } }
  469. const errorMessage =
  470. errorData.error?.message ||
  471. errorData.message ||
  472. errorData.error ||
  473. 'Failed to download image from URL';
  474. throw new Error(errorMessage);
  475. }
  476. const result = await response.json();
  477. return {
  478. mimeType: result.mime_type,
  479. filename: result.filename,
  480. base64Data: result.base64_data
  481. };
  482. }
  483. async cancelJob(jobId: string): Promise<void> {
  484. // Clear job status cache when cancelling
  485. cache.delete(`job_status_${jobId}`);
  486. return this.request<void>('/queue/cancel', {
  487. method: 'POST',
  488. body: JSON.stringify({ job_id: jobId }),
  489. });
  490. }
  491. // Get queue status with caching and throttling
  492. async getQueueStatus(): Promise<QueueStatus> {
  493. const cacheKey = 'queue_status';
  494. const cachedResult = cache.get(cacheKey);
  495. if (cachedResult) {
  496. return cachedResult;
  497. }
  498. const response = await this.request<{ queue: QueueStatus }>('/queue/status');
  499. // Cache queue status based on current activity
  500. const hasActiveJobs = response.queue.jobs.some(job =>
  501. job.status === 'processing' || job.status === 'queued'
  502. );
  503. // Cache for shorter time if there are active jobs
  504. const cacheTime = hasActiveJobs ? 1000 : 5000; // 1 second for active, 5 seconds for idle
  505. cache.set(cacheKey, response.queue, cacheTime);
  506. return response.queue;
  507. }
  508. async clearQueue(): Promise<void> {
  509. // Clear all related caches
  510. cache.delete('queue_status');
  511. return this.request<void>('/queue/clear', {
  512. method: 'POST',
  513. });
  514. }
  515. // Model management
  516. async getModels(type?: string, loaded?: boolean, page: number = 1, limit: number = -1, search?: string): Promise<EnhancedModelsResponse> {
  517. const cacheKey = `models_${type || 'all'}_${loaded ? 'loaded' : 'all'}_${page}_${limit}_${search || 'all'}`;
  518. const cachedResult = cache.get(cacheKey);
  519. if (cachedResult) {
  520. return cachedResult;
  521. }
  522. let endpoint = '/models';
  523. const params = [];
  524. if (type && type !== 'loaded') params.push(`type=${type}`);
  525. if (type === 'loaded' || loaded) params.push('loaded=true');
  526. // Only add page parameter if we're using pagination (limit > 0)
  527. if (limit > 0) {
  528. params.push(`page=${page}`);
  529. params.push(`limit=${limit}`);
  530. } else {
  531. // When limit is 0 (default), we want all models, so add limit=0 to disable pagination
  532. params.push('limit=0');
  533. }
  534. if (search) params.push(`search=${encodeURIComponent(search)}`);
  535. // Add include_metadata for additional information
  536. params.push('include_metadata=true');
  537. if (params.length > 0) endpoint += '?' + params.join('&');
  538. const response = await this.request<EnhancedModelsResponse>(endpoint);
  539. const models = response.models.map(model => ({
  540. ...model,
  541. id: model.sha256_short || model.name,
  542. size: model.file_size || model.size,
  543. path: model.path || model.name,
  544. }));
  545. const result = {
  546. ...response,
  547. models,
  548. };
  549. // Cache models for 30 seconds as they don't change frequently
  550. cache.set(cacheKey, result, 30000);
  551. return result;
  552. }
  553. // Get models with automatic selection information
  554. async getModelsForAutoSelection(checkpointModel?: string): Promise<EnhancedModelsResponse> {
  555. const cacheKey = `models_auto_selection_${checkpointModel || 'none'}`;
  556. const cachedResult = cache.get(cacheKey);
  557. if (cachedResult) {
  558. return cachedResult;
  559. }
  560. let endpoint = '/models';
  561. const params = [];
  562. params.push('include_metadata=true');
  563. params.push('include_requirements=true');
  564. if (checkpointModel) {
  565. params.push(`checkpoint=${encodeURIComponent(checkpointModel)}`);
  566. }
  567. params.push('limit=0'); // Get all models
  568. if (params.length > 0) endpoint += '?' + params.join('&');
  569. const response = await this.request<EnhancedModelsResponse>(endpoint);
  570. const models = response.models.map(model => ({
  571. ...model,
  572. id: model.sha256_short || model.name,
  573. size: model.file_size || model.size,
  574. path: model.path || model.name,
  575. }));
  576. const result = {
  577. ...response,
  578. models,
  579. };
  580. // Cache for 30 seconds
  581. cache.set(cacheKey, result, 30000);
  582. return result;
  583. }
  584. // Utility function to get models by type
  585. getModelsByType(models: ModelInfo[], type: string): ModelInfo[] {
  586. return models.filter(model =>
  587. model.type.toLowerCase() === type.toLowerCase()
  588. );
  589. }
  590. // Utility function to find models by name pattern
  591. findModelsByName(models: ModelInfo[], namePattern: string): ModelInfo[] {
  592. const pattern = namePattern.toLowerCase();
  593. return models.filter(model =>
  594. model.name.toLowerCase().includes(pattern)
  595. );
  596. }
  597. // Utility function to get loaded models by type
  598. getLoadedModelsByType(models: ModelInfo[], type: string): ModelInfo[] {
  599. return this.getModelsByType(models, type).filter(model => model.loaded);
  600. }
  601. // Get all models (for backward compatibility)
  602. async getAllModels(type?: string, loaded?: boolean): Promise<ModelInfo[]> {
  603. const allModels: ModelInfo[] = [];
  604. let page = 1;
  605. const limit = 100;
  606. while (true) {
  607. const response = await this.getModels(type, loaded, page, limit);
  608. allModels.push(...response.models);
  609. if (!response.pagination.has_next) {
  610. break;
  611. }
  612. page++;
  613. }
  614. return allModels;
  615. }
  616. async getModelInfo(modelId: string): Promise<ModelInfo> {
  617. const cacheKey = `model_info_${modelId}`;
  618. const cachedResult = cache.get(cacheKey);
  619. if (cachedResult) {
  620. return cachedResult;
  621. }
  622. const result = await this.request<ModelInfo>(`/models/${modelId}`);
  623. cache.set(cacheKey, result, 30000); // Cache for 30 seconds
  624. return result;
  625. }
  626. async loadModel(modelId: string): Promise<void> {
  627. // Clear model cache when loading
  628. cache.delete(`model_info_${modelId}`);
  629. return this.request<void>(`/models/${modelId}/load`, {
  630. method: 'POST',
  631. });
  632. }
  633. async unloadModel(modelId: string): Promise<void> {
  634. // Clear model cache when unloading
  635. cache.delete(`model_info_${modelId}`);
  636. return this.request<void>(`/models/${modelId}/unload`, {
  637. method: 'POST',
  638. });
  639. }
  640. async scanModels(): Promise<void> {
  641. // Clear all model caches when scanning
  642. cache.clear();
  643. return this.request<void>('/models/refresh', {
  644. method: 'POST',
  645. });
  646. }
  647. async getModelTypes(): Promise<Array<{ type: string; description: string; extensions: string[]; capabilities: string[]; requires?: string[]; recommended_for: string }>> {
  648. const cacheKey = 'model_types';
  649. const cachedResult = cache.get(cacheKey);
  650. if (cachedResult) {
  651. return cachedResult;
  652. }
  653. const response = await this.request<{ model_types: Array<{ type: string; description: string; extensions: string[]; capabilities: string[]; requires?: string[]; recommended_for: string }> }>('/models/types');
  654. cache.set(cacheKey, response.model_types, 60000); // Cache for 1 minute
  655. return response.model_types;
  656. }
  657. async convertModel(modelName: string, quantizationType: string, outputPath?: string): Promise<{ request_id: string; message: string }> {
  658. return this.request<{ request_id: string; message: string }>('/models/convert', {
  659. method: 'POST',
  660. body: JSON.stringify({
  661. model_name: modelName,
  662. quantization_type: quantizationType,
  663. output_path: outputPath,
  664. }),
  665. });
  666. }
  667. // System endpoints
  668. async getHealth(): Promise<{ status: string }> {
  669. return this.request<{ status: string }>('/health');
  670. }
  671. async getStatus(): Promise<any> {
  672. return this.request<any>('/status');
  673. }
  674. async getSystemInfo(): Promise<any> {
  675. return this.request<any>('/system');
  676. }
  677. async restartServer(): Promise<{ message: string }> {
  678. return this.request<{ message: string }>('/system/restart', {
  679. method: 'POST',
  680. body: JSON.stringify({}),
  681. });
  682. }
  683. // Image manipulation endpoints
  684. async resizeImage(image: string, width: number, height: number): Promise<{ image: string }> {
  685. return this.request<{ image: string }>('/image/resize', {
  686. method: 'POST',
  687. body: JSON.stringify({
  688. image,
  689. width,
  690. height,
  691. }),
  692. });
  693. }
  694. async cropImage(image: string, x: number, y: number, width: number, height: number): Promise<{ image: string }> {
  695. return this.request<{ image: string }>('/image/crop', {
  696. method: 'POST',
  697. body: JSON.stringify({
  698. image,
  699. x,
  700. y,
  701. width,
  702. height,
  703. }),
  704. });
  705. }
  706. // Configuration endpoints with caching
  707. async getSamplers(): Promise<Array<{ name: string; description: string; recommended_steps: number }>> {
  708. const cacheKey = 'samplers';
  709. const cachedResult = cache.get(cacheKey);
  710. if (cachedResult) {
  711. return cachedResult;
  712. }
  713. const response = await this.request<{ samplers: Array<{ name: string; description: string; recommended_steps: number }> }>('/samplers');
  714. cache.set(cacheKey, response.samplers, 60000); // Cache for 1 minute
  715. return response.samplers;
  716. }
  717. async getSchedulers(): Promise<Array<{ name: string; description: string }>> {
  718. const cacheKey = 'schedulers';
  719. const cachedResult = cache.get(cacheKey);
  720. if (cachedResult) {
  721. return cachedResult;
  722. }
  723. const response = await this.request<{ schedulers: Array<{ name: string; description: string }> }>('/schedulers');
  724. cache.set(cacheKey, response.schedulers, 60000); // Cache for 1 minute
  725. return response.schedulers;
  726. }
  727. // Cache management methods
  728. clearCache(): void {
  729. cache.clear();
  730. }
  731. clearCacheByPrefix(prefix: string): void {
  732. const keysToDelete: string[] = [];
  733. (cache as any).cache.forEach((_: any, key: string) => {
  734. if (key.startsWith(prefix)) {
  735. keysToDelete.push(key);
  736. }
  737. });
  738. keysToDelete.forEach(key => cache.delete(key));
  739. }
  740. }
  741. // Generic API request function for authentication
  742. export async function apiRequest(
  743. endpoint: string,
  744. options: RequestInit = {}
  745. ): Promise<Response> {
  746. const { apiUrl, apiBase } = getApiConfig();
  747. const url = `${apiUrl}${apiBase}${endpoint}`;
  748. // Get authentication method from server config
  749. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  750. ? window.__SERVER_CONFIG__.authMethod
  751. : 'jwt';
  752. // Add auth token or Unix user header based on auth method
  753. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  754. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  755. const headers: Record<string, string> = {
  756. 'Content-Type': 'application/json',
  757. ...options.headers as Record<string, string>,
  758. };
  759. if (authMethod === 'unix' && unixUser) {
  760. // For Unix auth, send the username in X-Unix-User header
  761. headers['X-Unix-User'] = unixUser;
  762. } else if (token) {
  763. // For JWT auth, send the token in Authorization header
  764. headers['Authorization'] = `Bearer ${token}`;
  765. }
  766. return fetch(url, {
  767. ...options,
  768. headers,
  769. });
  770. }
  771. // Authentication API endpoints
  772. export const authApi = {
  773. async login(username: string, password?: string) {
  774. // Get authentication method from server config
  775. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  776. ? window.__SERVER_CONFIG__.authMethod
  777. : 'jwt';
  778. // For both Unix and JWT auth, send username and password
  779. // The server will handle whether password is required based on PAM availability
  780. const response = await apiRequest('/auth/login', {
  781. method: 'POST',
  782. body: JSON.stringify({ username, password }),
  783. });
  784. if (!response.ok) {
  785. const error = await response.json().catch(() => ({ message: 'Login failed' }));
  786. throw new Error(error.message || 'Login failed');
  787. }
  788. return response.json();
  789. },
  790. async validateToken(token: string) {
  791. const response = await apiRequest('/auth/validate', {
  792. headers: { 'Authorization': `Bearer ${token}` },
  793. });
  794. if (!response.ok) {
  795. throw new Error('Token validation failed');
  796. }
  797. return response.json();
  798. },
  799. async refreshToken() {
  800. const response = await apiRequest('/auth/refresh', {
  801. method: 'POST',
  802. });
  803. if (!response.ok) {
  804. throw new Error('Token refresh failed');
  805. }
  806. return response.json();
  807. },
  808. async logout() {
  809. await apiRequest('/auth/logout', {
  810. method: 'POST',
  811. });
  812. },
  813. async getCurrentUser() {
  814. const response = await apiRequest('/auth/me');
  815. if (!response.ok) {
  816. throw new Error('Failed to get current user');
  817. }
  818. return response.json();
  819. },
  820. async changePassword(oldPassword: string, newPassword: string) {
  821. const response = await apiRequest('/auth/change-password', {
  822. method: 'POST',
  823. body: JSON.stringify({ old_password: oldPassword, new_password: newPassword }),
  824. });
  825. if (!response.ok) {
  826. const error = await response.json().catch(() => ({ message: 'Password change failed' }));
  827. throw new Error(error.message || 'Password change failed');
  828. }
  829. return response.json();
  830. },
  831. // API Key management
  832. async getApiKeys() {
  833. const response = await apiRequest('/auth/api-keys');
  834. if (!response.ok) {
  835. throw new Error('Failed to get API keys');
  836. }
  837. return response.json();
  838. },
  839. async createApiKey(name: string, scopes?: string[]) {
  840. const response = await apiRequest('/auth/api-keys', {
  841. method: 'POST',
  842. body: JSON.stringify({ name, scopes }),
  843. });
  844. if (!response.ok) {
  845. const error = await response.json().catch(() => ({ message: 'Failed to create API key' }));
  846. throw new Error(error.message || 'Failed to create API key');
  847. }
  848. return response.json();
  849. },
  850. async deleteApiKey(keyId: string) {
  851. const response = await apiRequest(`/auth/api-keys/${keyId}`, {
  852. method: 'DELETE',
  853. });
  854. if (!response.ok) {
  855. throw new Error('Failed to delete API key');
  856. }
  857. return response.json();
  858. },
  859. // User management (admin only)
  860. async getUsers() {
  861. const response = await apiRequest('/auth/users');
  862. if (!response.ok) {
  863. throw new Error('Failed to get users');
  864. }
  865. return response.json();
  866. },
  867. async createUser(userData: { username: string; email?: string; password: string; role?: string }) {
  868. const response = await apiRequest('/auth/users', {
  869. method: 'POST',
  870. body: JSON.stringify(userData),
  871. });
  872. if (!response.ok) {
  873. const error = await response.json().catch(() => ({ message: 'Failed to create user' }));
  874. throw new Error(error.message || 'Failed to create user');
  875. }
  876. return response.json();
  877. },
  878. async updateUser(userId: string, userData: { email?: string; role?: string; active?: boolean }) {
  879. const response = await apiRequest(`/auth/users/${userId}`, {
  880. method: 'PUT',
  881. body: JSON.stringify(userData),
  882. });
  883. if (!response.ok) {
  884. const error = await response.json().catch(() => ({ message: 'Failed to update user' }));
  885. throw new Error(error.message || 'Failed to update user');
  886. }
  887. return response.json();
  888. },
  889. async deleteUser(userId: string) {
  890. const response = await apiRequest(`/auth/users/${userId}`, {
  891. method: 'DELETE',
  892. });
  893. if (!response.ok) {
  894. throw new Error('Failed to delete user');
  895. }
  896. return response.json();
  897. }
  898. };
  899. export const apiClient = new ApiClient();