diff --git a/tools/server/webui/src/lib/services/models.service.ts b/tools/server/webui/src/lib/services/models.service.ts index 2cdaaf8dab..06d7ee2a37 100644 --- a/tools/server/webui/src/lib/services/models.service.ts +++ b/tools/server/webui/src/lib/services/models.service.ts @@ -1,6 +1,5 @@ -import { base } from '$app/paths'; import { ServerModelStatus } from '$lib/enums'; -import { getJsonHeaders } from '$lib/utils'; +import { apiFetch, apiPost } from '$lib/utils'; /** * ModelsService - Stateless service for model management API communication @@ -31,15 +30,7 @@ export class ModelsService { * Works in both MODEL and ROUTER modes */ static async list(): Promise { - const response = await fetch(`${base}/v1/models`, { - headers: getJsonHeaders() - }); - - if (!response.ok) { - throw new Error(`Failed to fetch model list (status ${response.status})`); - } - - return response.json() as Promise; + return apiFetch('/v1/models'); } /** @@ -47,15 +38,7 @@ export class ModelsService { * Returns models with load status, paths, and other metadata */ static async listRouter(): Promise { - const response = await fetch(`${base}/v1/models`, { - headers: getJsonHeaders() - }); - - if (!response.ok) { - throw new Error(`Failed to fetch router models list (status ${response.status})`); - } - - return response.json() as Promise; + return apiFetch('/v1/models'); } /** @@ -78,18 +61,7 @@ export class ModelsService { payload.extra_args = extraArgs; } - const response = await fetch(`${base}/models/load`, { - method: 'POST', - headers: getJsonHeaders(), - body: JSON.stringify(payload) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || `Failed to load model (status ${response.status})`); - } - - return response.json() as Promise; + return apiPost('/models/load', payload); } /** @@ -98,18 +70,7 @@ export class ModelsService { * @param modelId - Model identifier to unload */ static async unload(modelId: string): Promise { - const response = await fetch(`${base}/models/unload`, { - method: 'POST', - headers: getJsonHeaders(), - body: JSON.stringify({ model: modelId }) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || `Failed to unload model (status ${response.status})`); - } - - return response.json() as Promise; + return apiPost('/models/unload', { model: modelId }); } /** diff --git a/tools/server/webui/src/lib/services/props.service.ts b/tools/server/webui/src/lib/services/props.service.ts index 79f313ff99..534c703f38 100644 --- a/tools/server/webui/src/lib/services/props.service.ts +++ b/tools/server/webui/src/lib/services/props.service.ts @@ -1,4 +1,4 @@ -import { getAuthHeaders } from '$lib/utils'; +import { apiFetchWithParams } from '$lib/utils'; /** * PropsService - Server properties management @@ -31,23 +31,12 @@ export class PropsService { * @throws {Error} If the request fails or returns invalid data */ static async fetch(autoload = false): Promise { - const url = new URL('./props', window.location.href); + const params: Record = {}; if (!autoload) { - url.searchParams.set('autoload', 'false'); + params.autoload = 'false'; } - const response = await fetch(url.toString(), { - headers: getAuthHeaders() - }); - - if (!response.ok) { - throw new Error( - `Failed to fetch server properties: ${response.status} ${response.statusText}` - ); - } - - const data = await response.json(); - return data as ApiLlamaCppServerProps; + return apiFetchWithParams('./props', params, { authOnly: true }); } /** @@ -59,23 +48,11 @@ export class PropsService { * @throws {Error} If the request fails or returns invalid data */ static async fetchForModel(modelId: string, autoload = false): Promise { - const url = new URL('./props', window.location.href); - url.searchParams.set('model', modelId); + const params: Record = { model: modelId }; if (!autoload) { - url.searchParams.set('autoload', 'false'); + params.autoload = 'false'; } - const response = await fetch(url.toString(), { - headers: getAuthHeaders() - }); - - if (!response.ok) { - throw new Error( - `Failed to fetch model properties: ${response.status} ${response.statusText}` - ); - } - - const data = await response.json(); - return data as ApiLlamaCppServerProps; + return apiFetchWithParams('./props', params, { authOnly: true }); } } diff --git a/tools/server/webui/src/lib/utils/api-fetch.ts b/tools/server/webui/src/lib/utils/api-fetch.ts new file mode 100644 index 0000000000..3f3e6ee58d --- /dev/null +++ b/tools/server/webui/src/lib/utils/api-fetch.ts @@ -0,0 +1,154 @@ +import { base } from '$app/paths'; +import { getJsonHeaders, getAuthHeaders } from './api-headers'; + +/** + * API Fetch Utilities + * + * Provides common fetch patterns used across services: + * - Automatic JSON headers + * - Error handling with proper error messages + * - Base path resolution + */ + +export interface ApiFetchOptions extends Omit { + /** + * Use auth-only headers (no Content-Type). + * Default: false (uses JSON headers with Content-Type: application/json) + */ + authOnly?: boolean; + /** + * Additional headers to merge with default headers. + */ + headers?: Record; +} + +/** + * Fetch JSON data from an API endpoint with standard headers and error handling. + * + * @param path - API path (will be prefixed with base path) + * @param options - Fetch options with additional authOnly flag + * @returns Parsed JSON response + * @throws Error with formatted message on failure + * + * @example + * ```typescript + * // GET request + * const models = await apiFetch('/v1/models'); + * + * // POST request + * const result = await apiFetch('/models/load', { + * method: 'POST', + * body: JSON.stringify({ model: 'gpt-4' }) + * }); + * ``` + */ +export async function apiFetch(path: string, options: ApiFetchOptions = {}): Promise { + const { authOnly = false, headers: customHeaders, ...fetchOptions } = options; + + const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders(); + const headers = { ...baseHeaders, ...customHeaders }; + + const url = path.startsWith('http') ? path : `${base}${path}`; + + const response = await fetch(url, { + ...fetchOptions, + headers + }); + + if (!response.ok) { + const errorMessage = await parseErrorMessage(response); + throw new Error(errorMessage); + } + + return response.json() as Promise; +} + +/** + * Fetch with URL constructed from base URL and query parameters. + * + * @param basePath - Base API path + * @param params - Query parameters to append + * @param options - Fetch options + * @returns Parsed JSON response + * + * @example + * ```typescript + * const props = await apiFetchWithParams('./props', { + * model: 'gpt-4', + * autoload: 'false' + * }); + * ``` + */ +export async function apiFetchWithParams( + basePath: string, + params: Record, + options: ApiFetchOptions = {} +): Promise { + const url = new URL(basePath, window.location.href); + + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + url.searchParams.set(key, value); + } + } + + const { authOnly = false, headers: customHeaders, ...fetchOptions } = options; + + const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders(); + const headers = { ...baseHeaders, ...customHeaders }; + + const response = await fetch(url.toString(), { + ...fetchOptions, + headers + }); + + if (!response.ok) { + const errorMessage = await parseErrorMessage(response); + throw new Error(errorMessage); + } + + return response.json() as Promise; +} + +/** + * POST JSON data to an API endpoint. + * + * @param path - API path + * @param body - Request body (will be JSON stringified) + * @param options - Additional fetch options + * @returns Parsed JSON response + */ +export async function apiPost( + path: string, + body: B, + options: ApiFetchOptions = {} +): Promise { + return apiFetch(path, { + method: 'POST', + body: JSON.stringify(body), + ...options + }); +} + +/** + * Parse error message from a failed response. + * Tries to extract error message from JSON body, falls back to status text. + */ +async function parseErrorMessage(response: Response): Promise { + try { + const errorData = await response.json(); + if (errorData?.error?.message) { + return errorData.error.message; + } + if (errorData?.error && typeof errorData.error === 'string') { + return errorData.error; + } + if (errorData?.message) { + return errorData.message; + } + } catch { + // JSON parsing failed, use status text + } + + return `Request failed: ${response.status} ${response.statusText}`; +} diff --git a/tools/server/webui/src/lib/utils/index.ts b/tools/server/webui/src/lib/utils/index.ts index 1e32dc71ed..33c330396c 100644 --- a/tools/server/webui/src/lib/utils/index.ts +++ b/tools/server/webui/src/lib/utils/index.ts @@ -9,6 +9,7 @@ // API utilities export { getAuthHeaders, getJsonHeaders } from './api-headers'; +export { apiFetch, apiFetchWithParams, apiPost, type ApiFetchOptions } from './api-fetch'; export { validateApiKey } from './api-key-validation'; // Attachment utilities