char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: Stores split refactor (#27240)521a64cd0

char/aliceai-t5-moe
5.1 KiB199 linesraw
1import { getAuthHeaders, getJsonHeaders } from './api-headers';
2import { base } from '$app/paths';
3import { API_ABSOLUTE_URL_PROTOCOLS, ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants';
4
5/**
6 * API Fetch Utilities
7 *
8 * Provides common fetch patterns used across services:
9 * - Automatic JSON headers
10 * - Error handling with proper error messages
11 * - Base path resolution
12 */
13
14/**
15 * Error thrown when an API request fails, carrying the HTTP status code
16 * so callers can distinguish e.g. a 503 "still loading" response from a
17 * genuine failure.
18 */
19export class ApiError extends Error {
20	status: number;
21
22	constructor(message: string, status: number) {
23		super(message);
24		this.name = 'ApiError';
25		this.status = status;
26	}
27}
28
29export interface ApiFetchOptions extends Omit<RequestInit, 'headers'> {
30	/**
31	 * Use auth-only headers (no Content-Type).
32	 * Default: false (uses JSON headers with Content-Type: application/json)
33	 */
34	authOnly?: boolean;
35	/**
36	 * Additional headers to merge with default headers.
37	 */
38	headers?: Record<string, string>;
39}
40
41/**
42 * Fetch JSON data from an API endpoint with standard headers and error handling.
43 *
44 * @param path - API path (will be prefixed with base path)
45 * @param options - Fetch options with additional authOnly flag
46 * @returns Parsed JSON response
47 * @throws Error with formatted message on failure
48 *
49 * @example
50 * ```typescript
51 * // GET request
52 * const models = await apiFetch<ApiModelListResponse>('/v1/models');
53 *
54 * // POST request
55 * const result = await apiFetch<ApiResponse>('/models/load', {
56 *   method: 'POST',
57 *   body: JSON.stringify({ model: 'gpt-4' })
58 * });
59 * ```
60 */
61export async function apiFetch<T>(path: string, options: ApiFetchOptions = {}): Promise<T> {
62	const { authOnly = false, headers: customHeaders, ...fetchOptions } = options;
63	const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders();
64	const headers = { ...baseHeaders, ...customHeaders };
65	// absolute URLs with an allowed protocol pass through untouched; relative paths get the base prefix
66	const url = API_ABSOLUTE_URL_PROTOCOLS.some((p) => path.startsWith(p)) ? path : `${base}${path}`;
67
68	let response;
69
70	try {
71		response = await fetch(url, {
72			...fetchOptions,
73			headers
74		});
75	} catch (e) {
76		throw new Error(beautifyNetworkError(e));
77	}
78
79	if (!response.ok) {
80		const errorMessage = await parseErrorMessage(response);
81
82		throw new ApiError(errorMessage, response.status);
83	}
84
85	return response.json() as Promise<T>;
86}
87
88/**
89 * Fetch with URL constructed from base URL and query parameters.
90 *
91 * @param basePath - Base API path
92 * @param params - Query parameters to append
93 * @param options - Fetch options
94 * @returns Parsed JSON response
95 *
96 * @example
97 * ```typescript
98 * const props = await apiFetchWithParams<ApiProps>('./props', {
99 *   model: 'gpt-4',
100 *   autoload: 'false'
101 * });
102 * ```
103 */
104export async function apiFetchWithParams<T>(
105	basePath: string,
106	params: Record<string, string>,
107	options: ApiFetchOptions = {}
108): Promise<T> {
109	const url = new URL(basePath, window.location.href);
110
111	for (const [key, value] of Object.entries(params)) {
112		if (value !== undefined && value !== null) {
113			url.searchParams.set(key, value);
114		}
115	}
116
117	return apiFetch<T>(url.toString(), options);
118}
119
120/**
121 * POST JSON data to an API endpoint.
122 *
123 * @param path - API path
124 * @param body - Request body (will be JSON stringified)
125 * @param options - Additional fetch options
126 * @returns Parsed JSON response
127 */
128export async function apiPost<T, B = unknown>(
129	path: string,
130	body: B,
131	options: ApiFetchOptions = {}
132): Promise<T> {
133	return apiFetch<T>(path, {
134		body: JSON.stringify(body),
135		method: 'POST',
136		...options
137	});
138}
139
140/**
141 * Parse error message from a failed response.
142 * Tries to extract error message from JSON body, falls back to status text.
143 */
144async function parseErrorMessage(response: Response): Promise<string> {
145	try {
146		const errorData = await response.json();
147
148		if (errorData?.error?.message) {
149			return errorData.error.message;
150		}
151
152		if (errorData?.error && typeof errorData.error === 'string') {
153			return errorData.error;
154		}
155
156		if (errorData?.message) {
157			return errorData.message;
158		}
159	} catch {
160		// JSON parsing failed, use status text
161	}
162
163	const httpErrorStr = HTTP_CODE_TO_STRING[response.status];
164
165	if (httpErrorStr) {
166		return httpErrorStr;
167	}
168
169	return `${ERROR_MESSAGES.HTTP.GENERIC}: ${response.status} ${response.statusText}`;
170}
171
172/**
173 * Converts a network issue into a human-readable message.
174 * @param throwable - The throwable raised during fetch operation
175 * @returns Error in an human-readable format
176 */
177function beautifyNetworkError(throwable: unknown): string {
178	let message;
179
180	if (throwable instanceof Error) {
181		message = throwable.message;
182
183		if (throwable.name === 'TypeError' && message.includes('fetch')) {
184			return ERROR_MESSAGES.NETWORK.UNREACHABLE;
185		}
186	} else {
187		message = String(throwable);
188	}
189
190	if (message.includes('ECONNREFUSED')) {
191		return ERROR_MESSAGES.NETWORK.REFUSED;
192	} else if (message.includes('ENOTFOUND')) {
193		return ERROR_MESSAGES.NETWORK.NXDOMAIN;
194	} else if (message.includes('ETIMEDOUT')) {
195		return ERROR_MESSAGES.NETWORK.TIMEOUT;
196	}
197
198	return `${ERROR_MESSAGES.NETWORK.GENERIC} (${message})`;
199}