api.ts 18 KB

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