api.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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. class ApiClient {
  84. // Get base URL dynamically at runtime to ensure server config is loaded
  85. private getBaseUrl(): string {
  86. const { apiUrl, apiBase } = getApiConfig();
  87. return `${apiUrl}${apiBase}`;
  88. }
  89. private async request<T>(
  90. endpoint: string,
  91. options: RequestInit = {}
  92. ): Promise<T> {
  93. const url = `${this.getBaseUrl()}${endpoint}`;
  94. // Get authentication method from server config
  95. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  96. ? window.__SERVER_CONFIG__.authMethod
  97. : 'jwt';
  98. // Add auth token or Unix user header based on auth method
  99. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  100. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  101. const headers: Record<string, string> = {
  102. 'Content-Type': 'application/json',
  103. ...options.headers as Record<string, string>,
  104. };
  105. if (authMethod === 'unix' && unixUser) {
  106. // For Unix auth, send the username in X-Unix-User header
  107. headers['X-Unix-User'] = unixUser;
  108. } else if (token) {
  109. // For JWT auth, send the token in Authorization header
  110. headers['Authorization'] = `Bearer ${token}`;
  111. }
  112. const response = await fetch(url, {
  113. ...options,
  114. headers,
  115. });
  116. if (!response.ok) {
  117. const errorData = await response.json().catch(() => ({
  118. error: { message: response.statusText },
  119. }));
  120. // Handle nested error structure: { error: { message: "..." } }
  121. const errorMessage =
  122. errorData.error?.message ||
  123. errorData.message ||
  124. errorData.error ||
  125. 'API request failed';
  126. throw new Error(errorMessage);
  127. }
  128. return response.json();
  129. }
  130. // Generation endpoints
  131. async generateImage(params: GenerationRequest): Promise<JobInfo> {
  132. return this.request<JobInfo>('/generate/text2img', {
  133. method: 'POST',
  134. body: JSON.stringify(params),
  135. });
  136. }
  137. async text2img(params: GenerationRequest): Promise<JobInfo> {
  138. return this.request<JobInfo>('/generate/text2img', {
  139. method: 'POST',
  140. body: JSON.stringify(params),
  141. });
  142. }
  143. async img2img(params: GenerationRequest & { image: string }): Promise<JobInfo> {
  144. // Convert frontend field name to backend field name
  145. const backendParams = {
  146. ...params,
  147. init_image: params.image,
  148. image: undefined, // Remove the frontend field
  149. };
  150. return this.request<JobInfo>('/generate/img2img', {
  151. method: 'POST',
  152. body: JSON.stringify(backendParams),
  153. });
  154. }
  155. async inpainting(params: GenerationRequest & { source_image: string; mask_image: string }): Promise<JobInfo> {
  156. return this.request<JobInfo>('/generate/inpainting', {
  157. method: 'POST',
  158. body: JSON.stringify(params),
  159. });
  160. }
  161. // Job management
  162. async getJobStatus(jobId: string): Promise<JobInfo> {
  163. return this.request<JobInfo>(`/queue/job/${jobId}`);
  164. }
  165. // Get authenticated image URL with cache-busting
  166. getImageUrl(jobId: string, filename: string): string {
  167. const { apiUrl, apiBase } = getApiConfig();
  168. const baseUrl = `${apiUrl}${apiBase}`;
  169. // Add cache-busting timestamp
  170. const timestamp = Date.now();
  171. const url = `${baseUrl}/queue/job/${jobId}/output/${filename}?t=${timestamp}`;
  172. return url;
  173. }
  174. // Download image with authentication
  175. async downloadImage(jobId: string, filename: string): Promise<Blob> {
  176. const url = this.getImageUrl(jobId, filename);
  177. // Get authentication method from server config
  178. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  179. ? window.__SERVER_CONFIG__.authMethod
  180. : 'jwt';
  181. // Add auth token or Unix user header based on auth method
  182. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  183. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  184. const headers: Record<string, string> = {};
  185. if (authMethod === 'unix' && unixUser) {
  186. // For Unix auth, send the username in X-Unix-User header
  187. headers['X-Unix-User'] = unixUser;
  188. } else if (token) {
  189. // For JWT auth, send the token in Authorization header
  190. headers['Authorization'] = `Bearer ${token}`;
  191. }
  192. const response = await fetch(url, {
  193. headers,
  194. });
  195. if (!response.ok) {
  196. const errorData = await response.json().catch(() => ({
  197. error: { message: response.statusText },
  198. }));
  199. // Handle nested error structure: { error: { message: "..." } }
  200. const errorMessage =
  201. errorData.error?.message ||
  202. errorData.message ||
  203. errorData.error ||
  204. 'Failed to download image';
  205. throw new Error(errorMessage);
  206. }
  207. return response.blob();
  208. }
  209. async cancelJob(jobId: string): Promise<void> {
  210. return this.request<void>('/queue/cancel', {
  211. method: 'POST',
  212. body: JSON.stringify({ job_id: jobId }),
  213. });
  214. }
  215. async getQueueStatus(): Promise<QueueStatus> {
  216. const response = await this.request<{ queue: QueueStatus }>('/queue/status');
  217. return response.queue;
  218. }
  219. async clearQueue(): Promise<void> {
  220. return this.request<void>('/queue/clear', {
  221. method: 'POST',
  222. });
  223. }
  224. // Model management
  225. async getModels(type?: string, loaded?: boolean): Promise<ModelInfo[]> {
  226. let endpoint = '/models';
  227. const params = [];
  228. if (type && type !== 'loaded') params.push(`type=${type}`);
  229. if (type === 'loaded' || loaded) params.push('loaded=true');
  230. // Request a high limit to get all models (default is 50)
  231. params.push('limit=1000');
  232. if (params.length > 0) endpoint += '?' + params.join('&');
  233. const response = await this.request<{ models: ModelInfo[] }>(endpoint);
  234. // Add id field based on sha256_short or name, and normalize size field
  235. return response.models.map(model => ({
  236. ...model,
  237. id: model.sha256_short || model.name,
  238. size: model.file_size || model.size,
  239. path: model.path || model.name,
  240. }));
  241. }
  242. async getModelInfo(modelId: string): Promise<ModelInfo> {
  243. return this.request<ModelInfo>(`/models/${modelId}`);
  244. }
  245. async loadModel(modelId: string): Promise<void> {
  246. return this.request<void>(`/models/${modelId}/load`, {
  247. method: 'POST',
  248. });
  249. }
  250. async unloadModel(modelId: string): Promise<void> {
  251. return this.request<void>(`/models/${modelId}/unload`, {
  252. method: 'POST',
  253. });
  254. }
  255. async scanModels(): Promise<void> {
  256. return this.request<void>('/models/refresh', {
  257. method: 'POST',
  258. });
  259. }
  260. async convertModel(modelName: string, quantizationType: string, outputPath?: string): Promise<{ request_id: string; message: string }> {
  261. return this.request<{ request_id: string; message: string }>('/models/convert', {
  262. method: 'POST',
  263. body: JSON.stringify({
  264. model_name: modelName,
  265. quantization_type: quantizationType,
  266. output_path: outputPath,
  267. }),
  268. });
  269. }
  270. // System endpoints
  271. async getHealth(): Promise<{ status: string }> {
  272. return this.request<{ status: string }>('/health');
  273. }
  274. async getStatus(): Promise<any> {
  275. return this.request<any>('/status');
  276. }
  277. async getSystemInfo(): Promise<any> {
  278. return this.request<any>('/system');
  279. }
  280. async restartServer(): Promise<{ message: string }> {
  281. return this.request<{ message: string }>('/system/restart', {
  282. method: 'POST',
  283. body: JSON.stringify({}),
  284. });
  285. }
  286. // Configuration endpoints
  287. async getSamplers(): Promise<Array<{ name: string; description: string; recommended_steps: number }>> {
  288. const response = await this.request<{ samplers: Array<{ name: string; description: string; recommended_steps: number }> }>('/samplers');
  289. return response.samplers;
  290. }
  291. async getSchedulers(): Promise<Array<{ name: string; description: string }>> {
  292. const response = await this.request<{ schedulers: Array<{ name: string; description: string }> }>('/schedulers');
  293. return response.schedulers;
  294. }
  295. }
  296. // Generic API request function for authentication
  297. export async function apiRequest(
  298. endpoint: string,
  299. options: RequestInit = {}
  300. ): Promise<Response> {
  301. const { apiUrl, apiBase } = getApiConfig();
  302. const url = `${apiUrl}${apiBase}${endpoint}`;
  303. // Get authentication method from server config
  304. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  305. ? window.__SERVER_CONFIG__.authMethod
  306. : 'jwt';
  307. // Add auth token or Unix user header based on auth method
  308. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  309. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  310. const headers: Record<string, string> = {
  311. 'Content-Type': 'application/json',
  312. ...options.headers as Record<string, string>,
  313. };
  314. if (authMethod === 'unix' && unixUser) {
  315. // For Unix auth, send the username in X-Unix-User header
  316. headers['X-Unix-User'] = unixUser;
  317. } else if (token) {
  318. // For JWT auth, send the token in Authorization header
  319. headers['Authorization'] = `Bearer ${token}`;
  320. }
  321. return fetch(url, {
  322. ...options,
  323. headers,
  324. });
  325. }
  326. // Authentication API endpoints
  327. export const authApi = {
  328. async login(username: string, password?: string) {
  329. // Get authentication method from server config
  330. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  331. ? window.__SERVER_CONFIG__.authMethod
  332. : 'jwt';
  333. // For both Unix and JWT auth, send username and password
  334. // The server will handle whether password is required based on PAM availability
  335. const response = await apiRequest('/auth/login', {
  336. method: 'POST',
  337. body: JSON.stringify({ username, password }),
  338. });
  339. if (!response.ok) {
  340. const error = await response.json().catch(() => ({ message: 'Login failed' }));
  341. throw new Error(error.message || 'Login failed');
  342. }
  343. return response.json();
  344. },
  345. async validateToken(token: string) {
  346. const response = await apiRequest('/auth/validate', {
  347. headers: { 'Authorization': `Bearer ${token}` },
  348. });
  349. if (!response.ok) {
  350. throw new Error('Token validation failed');
  351. }
  352. return response.json();
  353. },
  354. async refreshToken() {
  355. const response = await apiRequest('/auth/refresh', {
  356. method: 'POST',
  357. });
  358. if (!response.ok) {
  359. throw new Error('Token refresh failed');
  360. }
  361. return response.json();
  362. },
  363. async logout() {
  364. await apiRequest('/auth/logout', {
  365. method: 'POST',
  366. });
  367. },
  368. async getCurrentUser() {
  369. const response = await apiRequest('/auth/me');
  370. if (!response.ok) {
  371. throw new Error('Failed to get current user');
  372. }
  373. return response.json();
  374. },
  375. async changePassword(oldPassword: string, newPassword: string) {
  376. const response = await apiRequest('/auth/change-password', {
  377. method: 'POST',
  378. body: JSON.stringify({ old_password: oldPassword, new_password: newPassword }),
  379. });
  380. if (!response.ok) {
  381. const error = await response.json().catch(() => ({ message: 'Password change failed' }));
  382. throw new Error(error.message || 'Password change failed');
  383. }
  384. return response.json();
  385. },
  386. // API Key management
  387. async getApiKeys() {
  388. const response = await apiRequest('/auth/api-keys');
  389. if (!response.ok) {
  390. throw new Error('Failed to get API keys');
  391. }
  392. return response.json();
  393. },
  394. async createApiKey(name: string, scopes?: string[]) {
  395. const response = await apiRequest('/auth/api-keys', {
  396. method: 'POST',
  397. body: JSON.stringify({ name, scopes }),
  398. });
  399. if (!response.ok) {
  400. const error = await response.json().catch(() => ({ message: 'Failed to create API key' }));
  401. throw new Error(error.message || 'Failed to create API key');
  402. }
  403. return response.json();
  404. },
  405. async deleteApiKey(keyId: string) {
  406. const response = await apiRequest(`/auth/api-keys/${keyId}`, {
  407. method: 'DELETE',
  408. });
  409. if (!response.ok) {
  410. throw new Error('Failed to delete API key');
  411. }
  412. return response.json();
  413. },
  414. // User management (admin only)
  415. async getUsers() {
  416. const response = await apiRequest('/auth/users');
  417. if (!response.ok) {
  418. throw new Error('Failed to get users');
  419. }
  420. return response.json();
  421. },
  422. async createUser(userData: { username: string; email?: string; password: string; role?: string }) {
  423. const response = await apiRequest('/auth/users', {
  424. method: 'POST',
  425. body: JSON.stringify(userData),
  426. });
  427. if (!response.ok) {
  428. const error = await response.json().catch(() => ({ message: 'Failed to create user' }));
  429. throw new Error(error.message || 'Failed to create user');
  430. }
  431. return response.json();
  432. },
  433. async updateUser(userId: string, userData: { email?: string; role?: string; active?: boolean }) {
  434. const response = await apiRequest(`/auth/users/${userId}`, {
  435. method: 'PUT',
  436. body: JSON.stringify(userData),
  437. });
  438. if (!response.ok) {
  439. const error = await response.json().catch(() => ({ message: 'Failed to update user' }));
  440. throw new Error(error.message || 'Failed to update user');
  441. }
  442. return response.json();
  443. },
  444. async deleteUser(userId: string) {
  445. const response = await apiRequest(`/auth/users/${userId}`, {
  446. method: 'DELETE',
  447. });
  448. if (!response.ok) {
  449. throw new Error('Failed to delete user');
  450. }
  451. return response.json();
  452. }
  453. };
  454. export const apiClient = new ApiClient();