api.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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. // Get configuration from server-injected config or fallback to environment/defaults
  16. // This function is called at runtime to ensure __SERVER_CONFIG__ is available
  17. function getApiConfig() {
  18. if (typeof window !== 'undefined' && window.__SERVER_CONFIG__) {
  19. return {
  20. apiUrl: window.__SERVER_CONFIG__.apiUrl,
  21. apiBase: window.__SERVER_CONFIG__.apiBasePath,
  22. };
  23. }
  24. // Fallback for development mode - use current window location
  25. if (typeof window !== 'undefined') {
  26. const protocol = window.location.protocol;
  27. const host = window.location.hostname;
  28. const port = window.location.port;
  29. return {
  30. apiUrl: `${protocol}//${host}:${port}`,
  31. apiBase: '/api',
  32. };
  33. }
  34. // Server-side fallback
  35. return {
  36. apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8081',
  37. apiBase: process.env.NEXT_PUBLIC_API_BASE_PATH || '/api',
  38. };
  39. }
  40. export interface GenerationRequest {
  41. model?: string;
  42. prompt: string;
  43. negative_prompt?: string;
  44. width?: number;
  45. height?: number;
  46. steps?: number;
  47. cfg_scale?: number;
  48. seed?: string;
  49. sampling_method?: string;
  50. scheduler?: string;
  51. batch_count?: number;
  52. clip_skip?: number;
  53. strength?: number;
  54. control_strength?: number;
  55. }
  56. export interface JobInfo {
  57. id?: string;
  58. request_id?: string;
  59. status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'queued';
  60. progress?: number;
  61. result?: {
  62. images: string[];
  63. };
  64. outputs?: Array<{
  65. filename: string;
  66. url: string;
  67. path: string;
  68. }>;
  69. error?: string;
  70. created_at?: string;
  71. updated_at?: string;
  72. message?: string;
  73. queue_position?: number;
  74. }
  75. export interface ModelInfo {
  76. id?: string;
  77. name: string;
  78. path?: string;
  79. type: string;
  80. size?: number;
  81. file_size?: number;
  82. file_size_mb?: number;
  83. sha256?: string | null;
  84. sha256_short?: string | null;
  85. loaded?: boolean;
  86. }
  87. export interface QueueStatus {
  88. active_generations: number;
  89. jobs: JobInfo[];
  90. running: boolean;
  91. size: number;
  92. }
  93. export interface HealthStatus {
  94. status: 'ok' | 'error' | 'degraded';
  95. message: string;
  96. timestamp: string;
  97. uptime?: number;
  98. version?: string;
  99. }
  100. class ApiClient {
  101. // Get base URL dynamically at runtime to ensure server config is loaded
  102. private getBaseUrl(): string {
  103. const { apiUrl, apiBase } = getApiConfig();
  104. return `${apiUrl}${apiBase}`;
  105. }
  106. private async request<T>(
  107. endpoint: string,
  108. options: RequestInit = {}
  109. ): Promise<T> {
  110. const url = `${this.getBaseUrl()}${endpoint}`;
  111. // Get authentication method from server config
  112. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  113. ? window.__SERVER_CONFIG__.authMethod
  114. : 'jwt';
  115. // Add auth token or Unix user header based on auth method
  116. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  117. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  118. const headers: Record<string, string> = {
  119. 'Content-Type': 'application/json',
  120. ...options.headers as Record<string, string>,
  121. };
  122. if (authMethod === 'unix' && unixUser) {
  123. // For Unix auth, send the username in X-Unix-User header
  124. headers['X-Unix-User'] = unixUser;
  125. } else if (token) {
  126. // For JWT auth, send the token in Authorization header
  127. headers['Authorization'] = `Bearer ${token}`;
  128. }
  129. const response = await fetch(url, {
  130. ...options,
  131. headers,
  132. });
  133. if (!response.ok) {
  134. const errorData = await response.json().catch(() => ({
  135. error: { message: response.statusText },
  136. }));
  137. // Handle nested error structure: { error: { message: "..." } }
  138. const errorMessage =
  139. errorData.error?.message ||
  140. errorData.message ||
  141. errorData.error ||
  142. 'API request failed';
  143. throw new Error(errorMessage);
  144. }
  145. return response.json();
  146. }
  147. // Enhanced health check with multiple endpoints and better error handling
  148. async checkHealth(): Promise<HealthStatus> {
  149. const endpoints = ['/queue/status', '/health', '/status', '/'];
  150. for (const endpoint of endpoints) {
  151. try {
  152. const response = await fetch(`${this.getBaseUrl()}${endpoint}`, {
  153. method: 'GET',
  154. headers: {
  155. 'Content-Type': 'application/json',
  156. },
  157. // Add timeout to prevent hanging requests
  158. signal: AbortSignal.timeout(5000),
  159. });
  160. if (response.ok) {
  161. const data = await response.json();
  162. // For queue status, consider it healthy if it returns valid structure
  163. if (endpoint === '/queue/status' && data.queue) {
  164. return {
  165. status: 'ok',
  166. message: 'API is running and queue is accessible',
  167. timestamp: new Date().toISOString(),
  168. };
  169. }
  170. // For other health endpoints
  171. const healthStatus: HealthStatus = {
  172. status: 'ok',
  173. message: 'API is running',
  174. timestamp: new Date().toISOString(),
  175. uptime: data.uptime,
  176. version: data.version || data.build || data.git_version,
  177. };
  178. // Handle different response formats
  179. if (data.status) {
  180. if (data.status === 'healthy' || data.status === 'running' || data.status === 'ok') {
  181. healthStatus.status = 'ok';
  182. healthStatus.message = data.message || 'API is running';
  183. } else if (data.status === 'degraded') {
  184. healthStatus.status = 'degraded';
  185. healthStatus.message = data.message || 'API is running in degraded mode';
  186. } else {
  187. healthStatus.status = 'error';
  188. healthStatus.message = data.message || 'API status is unknown';
  189. }
  190. }
  191. return healthStatus;
  192. }
  193. } catch (error) {
  194. // Continue to next endpoint if this one fails
  195. console.warn(`Health check failed for endpoint ${endpoint}:`, error);
  196. continue;
  197. }
  198. }
  199. // If all endpoints fail
  200. throw new Error('All health check endpoints are unavailable');
  201. }
  202. // Alternative simple connectivity check
  203. async checkConnectivity(): Promise<boolean> {
  204. try {
  205. const response = await fetch(`${this.getBaseUrl()}`, {
  206. method: 'HEAD',
  207. signal: AbortSignal.timeout(3000),
  208. });
  209. return response.ok || response.status < 500; // Accept any non-server-error response
  210. } catch (error) {
  211. return false;
  212. }
  213. }
  214. // Generation endpoints
  215. async generateImage(params: GenerationRequest): Promise<JobInfo> {
  216. return this.request<JobInfo>('/generate/text2img', {
  217. method: 'POST',
  218. body: JSON.stringify(params),
  219. });
  220. }
  221. async text2img(params: GenerationRequest): Promise<JobInfo> {
  222. return this.request<JobInfo>('/generate/text2img', {
  223. method: 'POST',
  224. body: JSON.stringify(params),
  225. });
  226. }
  227. async img2img(params: GenerationRequest & { image: string }): Promise<JobInfo> {
  228. // Convert frontend field name to backend field name
  229. const backendParams = {
  230. ...params,
  231. init_image: params.image,
  232. image: undefined, // Remove the frontend field
  233. };
  234. return this.request<JobInfo>('/generate/img2img', {
  235. method: 'POST',
  236. body: JSON.stringify(backendParams),
  237. });
  238. }
  239. async inpainting(params: GenerationRequest & { source_image: string; mask_image: string }): Promise<JobInfo> {
  240. return this.request<JobInfo>('/generate/inpainting', {
  241. method: 'POST',
  242. body: JSON.stringify(params),
  243. });
  244. }
  245. // Job management
  246. async getJobStatus(jobId: string): Promise<JobInfo> {
  247. return this.request<JobInfo>(`/queue/job/${jobId}`);
  248. }
  249. // Get authenticated image URL with cache-busting
  250. getImageUrl(jobId: string, filename: string): string {
  251. const { apiUrl, apiBase } = getApiConfig();
  252. const baseUrl = `${apiUrl}${apiBase}`;
  253. // Add cache-busting timestamp
  254. const timestamp = Date.now();
  255. const url = `${baseUrl}/queue/job/${jobId}/output/${filename}?t=${timestamp}`;
  256. return url;
  257. }
  258. // Download image with authentication
  259. async downloadImage(jobId: string, filename: string): Promise<Blob> {
  260. const url = this.getImageUrl(jobId, filename);
  261. // Get authentication method from server config
  262. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  263. ? window.__SERVER_CONFIG__.authMethod
  264. : 'jwt';
  265. // Add auth token or Unix user header based on auth method
  266. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  267. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  268. const headers: Record<string, string> = {};
  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. headers,
  278. });
  279. if (!response.ok) {
  280. const errorData = await response.json().catch(() => ({
  281. error: { message: response.statusText },
  282. }));
  283. // Handle nested error structure: { error: { message: "..." } }
  284. const errorMessage =
  285. errorData.error?.message ||
  286. errorData.message ||
  287. errorData.error ||
  288. 'Failed to download image';
  289. throw new Error(errorMessage);
  290. }
  291. return response.blob();
  292. }
  293. async cancelJob(jobId: string): Promise<void> {
  294. return this.request<void>('/queue/cancel', {
  295. method: 'POST',
  296. body: JSON.stringify({ job_id: jobId }),
  297. });
  298. }
  299. async getQueueStatus(): Promise<QueueStatus> {
  300. const response = await this.request<{ queue: QueueStatus }>('/queue/status');
  301. return response.queue;
  302. }
  303. async clearQueue(): Promise<void> {
  304. return this.request<void>('/queue/clear', {
  305. method: 'POST',
  306. });
  307. }
  308. // Model management
  309. async getModels(type?: string, loaded?: boolean): Promise<ModelInfo[]> {
  310. let endpoint = '/models';
  311. const params = [];
  312. if (type && type !== 'loaded') params.push(`type=${type}`);
  313. if (type === 'loaded' || loaded) params.push('loaded=true');
  314. // Request a high limit to get all models (default is 50)
  315. params.push('limit=1000');
  316. if (params.length > 0) endpoint += '?' + params.join('&');
  317. const response = await this.request<{ models: ModelInfo[] }>(endpoint);
  318. // Add id field based on sha256_short or name, and normalize size field
  319. return response.models.map(model => ({
  320. ...model,
  321. id: model.sha256_short || model.name,
  322. size: model.file_size || model.size,
  323. path: model.path || model.name,
  324. }));
  325. }
  326. async getModelInfo(modelId: string): Promise<ModelInfo> {
  327. return this.request<ModelInfo>(`/models/${modelId}`);
  328. }
  329. async loadModel(modelId: string): Promise<void> {
  330. return this.request<void>(`/models/${modelId}/load`, {
  331. method: 'POST',
  332. });
  333. }
  334. async unloadModel(modelId: string): Promise<void> {
  335. return this.request<void>(`/models/${modelId}/unload`, {
  336. method: 'POST',
  337. });
  338. }
  339. async scanModels(): Promise<void> {
  340. return this.request<void>('/models/refresh', {
  341. method: 'POST',
  342. });
  343. }
  344. async convertModel(modelName: string, quantizationType: string, outputPath?: string): Promise<{ request_id: string; message: string }> {
  345. return this.request<{ request_id: string; message: string }>('/models/convert', {
  346. method: 'POST',
  347. body: JSON.stringify({
  348. model_name: modelName,
  349. quantization_type: quantizationType,
  350. output_path: outputPath,
  351. }),
  352. });
  353. }
  354. // System endpoints
  355. async getHealth(): Promise<{ status: string }> {
  356. return this.request<{ status: string }>('/health');
  357. }
  358. async getStatus(): Promise<any> {
  359. return this.request<any>('/status');
  360. }
  361. async getSystemInfo(): Promise<any> {
  362. return this.request<any>('/system');
  363. }
  364. async restartServer(): Promise<{ message: string }> {
  365. return this.request<{ message: string }>('/system/restart', {
  366. method: 'POST',
  367. body: JSON.stringify({}),
  368. });
  369. }
  370. // Configuration endpoints
  371. async getSamplers(): Promise<Array<{ name: string; description: string; recommended_steps: number }>> {
  372. const response = await this.request<{ samplers: Array<{ name: string; description: string; recommended_steps: number }> }>('/samplers');
  373. return response.samplers;
  374. }
  375. async getSchedulers(): Promise<Array<{ name: string; description: string }>> {
  376. const response = await this.request<{ schedulers: Array<{ name: string; description: string }> }>('/schedulers');
  377. return response.schedulers;
  378. }
  379. }
  380. // Generic API request function for authentication
  381. export async function apiRequest(
  382. endpoint: string,
  383. options: RequestInit = {}
  384. ): Promise<Response> {
  385. const { apiUrl, apiBase } = getApiConfig();
  386. const url = `${apiUrl}${apiBase}${endpoint}`;
  387. // Get authentication method from server config
  388. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  389. ? window.__SERVER_CONFIG__.authMethod
  390. : 'jwt';
  391. // Add auth token or Unix user header based on auth method
  392. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  393. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  394. const headers: Record<string, string> = {
  395. 'Content-Type': 'application/json',
  396. ...options.headers as Record<string, string>,
  397. };
  398. if (authMethod === 'unix' && unixUser) {
  399. // For Unix auth, send the username in X-Unix-User header
  400. headers['X-Unix-User'] = unixUser;
  401. } else if (token) {
  402. // For JWT auth, send the token in Authorization header
  403. headers['Authorization'] = `Bearer ${token}`;
  404. }
  405. return fetch(url, {
  406. ...options,
  407. headers,
  408. });
  409. }
  410. // Authentication API endpoints
  411. export const authApi = {
  412. async login(username: string, password?: string) {
  413. // Get authentication method from server config
  414. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  415. ? window.__SERVER_CONFIG__.authMethod
  416. : 'jwt';
  417. // For both Unix and JWT auth, send username and password
  418. // The server will handle whether password is required based on PAM availability
  419. const response = await apiRequest('/auth/login', {
  420. method: 'POST',
  421. body: JSON.stringify({ username, password }),
  422. });
  423. if (!response.ok) {
  424. const error = await response.json().catch(() => ({ message: 'Login failed' }));
  425. throw new Error(error.message || 'Login failed');
  426. }
  427. return response.json();
  428. },
  429. async validateToken(token: string) {
  430. const response = await apiRequest('/auth/validate', {
  431. headers: { 'Authorization': `Bearer ${token}` },
  432. });
  433. if (!response.ok) {
  434. throw new Error('Token validation failed');
  435. }
  436. return response.json();
  437. },
  438. async refreshToken() {
  439. const response = await apiRequest('/auth/refresh', {
  440. method: 'POST',
  441. });
  442. if (!response.ok) {
  443. throw new Error('Token refresh failed');
  444. }
  445. return response.json();
  446. },
  447. async logout() {
  448. await apiRequest('/auth/logout', {
  449. method: 'POST',
  450. });
  451. },
  452. async getCurrentUser() {
  453. const response = await apiRequest('/auth/me');
  454. if (!response.ok) {
  455. throw new Error('Failed to get current user');
  456. }
  457. return response.json();
  458. },
  459. async changePassword(oldPassword: string, newPassword: string) {
  460. const response = await apiRequest('/auth/change-password', {
  461. method: 'POST',
  462. body: JSON.stringify({ old_password: oldPassword, new_password: newPassword }),
  463. });
  464. if (!response.ok) {
  465. const error = await response.json().catch(() => ({ message: 'Password change failed' }));
  466. throw new Error(error.message || 'Password change failed');
  467. }
  468. return response.json();
  469. },
  470. // API Key management
  471. async getApiKeys() {
  472. const response = await apiRequest('/auth/api-keys');
  473. if (!response.ok) {
  474. throw new Error('Failed to get API keys');
  475. }
  476. return response.json();
  477. },
  478. async createApiKey(name: string, scopes?: string[]) {
  479. const response = await apiRequest('/auth/api-keys', {
  480. method: 'POST',
  481. body: JSON.stringify({ name, scopes }),
  482. });
  483. if (!response.ok) {
  484. const error = await response.json().catch(() => ({ message: 'Failed to create API key' }));
  485. throw new Error(error.message || 'Failed to create API key');
  486. }
  487. return response.json();
  488. },
  489. async deleteApiKey(keyId: string) {
  490. const response = await apiRequest(`/auth/api-keys/${keyId}`, {
  491. method: 'DELETE',
  492. });
  493. if (!response.ok) {
  494. throw new Error('Failed to delete API key');
  495. }
  496. return response.json();
  497. },
  498. // User management (admin only)
  499. async getUsers() {
  500. const response = await apiRequest('/auth/users');
  501. if (!response.ok) {
  502. throw new Error('Failed to get users');
  503. }
  504. return response.json();
  505. },
  506. async createUser(userData: { username: string; email?: string; password: string; role?: string }) {
  507. const response = await apiRequest('/auth/users', {
  508. method: 'POST',
  509. body: JSON.stringify(userData),
  510. });
  511. if (!response.ok) {
  512. const error = await response.json().catch(() => ({ message: 'Failed to create user' }));
  513. throw new Error(error.message || 'Failed to create user');
  514. }
  515. return response.json();
  516. },
  517. async updateUser(userId: string, userData: { email?: string; role?: string; active?: boolean }) {
  518. const response = await apiRequest(`/auth/users/${userId}`, {
  519. method: 'PUT',
  520. body: JSON.stringify(userData),
  521. });
  522. if (!response.ok) {
  523. const error = await response.json().catch(() => ({ message: 'Failed to update user' }));
  524. throw new Error(error.message || 'Failed to update user');
  525. }
  526. return response.json();
  527. },
  528. async deleteUser(userId: string) {
  529. const response = await apiRequest(`/auth/users/${userId}`, {
  530. method: 'DELETE',
  531. });
  532. if (!response.ok) {
  533. throw new Error('Failed to delete user');
  534. }
  535. return response.json();
  536. }
  537. };
  538. export const apiClient = new ApiClient();