ui : resolve API requests against the active backend

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-16 19:22:47 +02:00
parent 31031a0cd2
commit 0ce4a9cef9
9 changed files with 44 additions and 32 deletions
@@ -1,13 +1,13 @@
<script lang="ts">
import { AlertTriangle, CheckCircle, Key, RefreshCw, XCircle } from '@lucide/svelte';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import Label from '$lib/components/ui/label/label.svelte';
import { HEADERS, ICON_CLASS_DEFAULT, ROUTES, SETTINGS_KEYS } from '$lib/constants';
import { KeyboardKey } from '$lib/enums';
import { serverStore, settingsStore } from '$lib/stores';
import { apiUrl } from '$lib/utils/api-base';
import { fade, fly, scale } from 'svelte/transition';
interface Props {
@@ -67,7 +67,7 @@
settingsStore.updateConfig(SETTINGS_KEYS.API_KEY, apiKeyInput.trim());
// Test the API key by making a real request to the server
const response = await fetch(`${base}/props`, {
const response = await fetch(apiUrl('/props'), {
headers: {
'Content-Type': 'application/json',
[HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKeyInput.trim()}`
+7 -6
View File
@@ -42,6 +42,7 @@ import type {
ApiStreamSession
} from '$lib/types/api';
import { isAbortError } from '$lib/utils/abort';
import { apiUrl } from '$lib/utils/api-base';
import { ApiError } from '$lib/utils/api-fetch';
import { getAuthHeaders, getJsonHeaders } from '$lib/utils/api-headers';
import { formatAttachmentText } from '$lib/utils/formatters';
@@ -88,7 +89,7 @@ export class ChatService {
static async areAllSlotsIdle(model?: string | null, signal?: AbortSignal): Promise<boolean> {
try {
const url = model ? `${API_SLOTS.LIST}?model=${encodeURIComponent(model)}` : API_SLOTS.LIST;
const res = await fetch(url, { signal });
const res = await fetch(apiUrl(url), { signal });
if (!res.ok) return true;
@@ -781,7 +782,7 @@ export class ChatService {
* conv::model identity when a model was bound at POST time.
*/
static async lookupStreamSessions(conversationIds: string[]): Promise<ApiStreamSession[]> {
const resp = await fetch(API_STREAM.LOOKUP, {
const resp = await fetch(apiUrl(API_STREAM.LOOKUP), {
body: JSON.stringify({ conversation_ids: conversationIds }),
headers: getJsonHeaders(),
method: 'POST'
@@ -869,7 +870,7 @@ export class ChatService {
}
try {
await fetch(API_CHAT.COMPLETIONS, {
await fetch(apiUrl(API_CHAT.COMPLETIONS), {
body: JSON.stringify(requestBody),
headers: getJsonHeaders(),
method: 'POST',
@@ -1230,7 +1231,7 @@ export class ChatService {
ChatService.saveStreamState(conversationId, 0, options.model ?? null);
}
const response = await fetch(API_CHAT.COMPLETIONS, {
const response = await fetch(apiUrl(API_CHAT.COMPLETIONS), {
body: JSON.stringify(requestBody),
headers,
method: 'POST',
@@ -1342,7 +1343,7 @@ export class ChatService {
if (model) body.model = model;
try {
const res = await fetch(API_CHAT.CONTROL, {
const res = await fetch(apiUrl(API_CHAT.CONTROL), {
body: JSON.stringify(body),
headers: getJsonHeaders(),
method: 'POST'
@@ -1373,7 +1374,7 @@ export class ChatService {
const query = `${STREAM_QUERY_PARAMS.CONV_ID}=${encodeURIComponent(streamId)}`;
const offset = from === undefined ? '' : `&${STREAM_QUERY_PARAMS.FROM}=${from}`;
return `${API_STREAM.BASE}?${query}${offset}`;
return apiUrl(`${API_STREAM.BASE}?${query}${offset}`);
}
/**
+2 -2
View File
@@ -6,7 +6,6 @@
* modelsStore and its status manager.
*/
import { base } from '$app/paths';
import { API_MODELS, MODEL_ID, type ModelSidecar, SIDECAR_TOKENS } from '$lib/constants';
import { ServerModelStatus } from '$lib/enums';
import type { ParsedModelId } from '$lib/types/models';
@@ -14,6 +13,7 @@ import {
apiDelete,
apiFetch,
apiPost,
apiUrl,
extractSseDataPayload,
normalizeModelName,
sidecarFromFileToken,
@@ -345,7 +345,7 @@ export class ModelsService {
while (!signal.aborted) {
try {
const response = await fetch(`${base}${API_MODELS.SSE}`, {
const response = await fetch(apiUrl(API_MODELS.SSE), {
headers: getAuthHeaders(),
signal
});
+2 -3
View File
@@ -5,11 +5,10 @@
* No reactive state; consumed by toolsStore.
*/
import { base } from '$app/paths';
import { API_TOOLS, HEADERS } from '$lib/constants';
import { ToolResponseField } from '$lib/enums';
import type { ServerToolInfo, ToolExecutionResult } from '$lib/types';
import { apiFetch } from '$lib/utils';
import { apiFetch, apiUrl } from '$lib/utils';
import { getJsonHeaders } from '$lib/utils/api-headers';
import { parseSseJsonStream, type SseJsonEvent } from '$lib/utils/sse';
@@ -108,7 +107,7 @@ export class ToolsService {
if (cwd) headers[HEADERS.X_TOOL_CWD_HEADER] = cwd;
const response = await fetch(`${base}${API_TOOLS.EXECUTE}`, {
const response = await fetch(apiUrl(API_TOOLS.EXECUTE), {
body: JSON.stringify({ params, stream: true, tool: toolName }),
headers,
method: 'POST',
+4 -5
View File
@@ -1,6 +1,6 @@
import { apiUrl } from './api-base';
import { getAuthHeaders, getJsonHeaders } from './api-headers';
import { base } from '$app/paths';
import { API_ABSOLUTE_URL_PROTOCOLS, ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants';
import { ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants';
/**
* API Fetch Utilities
@@ -62,8 +62,7 @@ export async function apiFetch<T>(path: string, options: ApiFetchOptions = {}):
const { authOnly = false, headers: customHeaders, ...fetchOptions } = options;
const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders();
const headers = { ...baseHeaders, ...customHeaders };
// absolute URLs with an allowed protocol pass through untouched; relative paths get the base prefix
const url = API_ABSOLUTE_URL_PROTOCOLS.some((p) => path.startsWith(p)) ? path : `${base}${path}`;
const url = apiUrl(path);
let response;
@@ -106,7 +105,7 @@ export async function apiFetchWithParams<T>(
params: Record<string, string>,
options: ApiFetchOptions = {}
): Promise<T> {
const url = new URL(basePath, window.location.href);
const url = new URL(apiUrl(basePath), window.location.href);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
+21 -8
View File
@@ -1,15 +1,16 @@
import { getBackend } from './api-base';
import { redactValue } from './redact';
import { CORS_PROXY, HEADERS } from '$lib/constants';
import { CORS_PROXY, HEADERS, LOCAL_BACKEND_ID } from '$lib/constants';
import { MimeTypeApplication } from '$lib/enums';
import { settingsStore } from '$lib/stores/settings/index.svelte';
/**
* Get authorization headers for API requests
* Includes Bearer token if API key is configured
* Get authorization headers for API requests to a backend.
* External backends carry their own key; the local backend reuses the global
* API key setting.
*/
export function getAuthHeaders(): Record<string, string> {
const currentConfig = settingsStore.config;
const apiKey = currentConfig.apiKey?.toString().trim();
export function getAuthHeaders(backendId?: string): Record<string, string> {
const apiKey = resolveBackendApiKey(backendId);
return apiKey ? { [HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey}` } : {};
}
@@ -17,13 +18,25 @@ export function getAuthHeaders(): Record<string, string> {
/**
* Get standard JSON headers with optional authorization
*/
export function getJsonHeaders(): Record<string, string> {
export function getJsonHeaders(backendId?: string): Record<string, string> {
return {
[HEADERS.CONTENT_TYPE]: MimeTypeApplication.JSON,
...getAuthHeaders()
...getAuthHeaders(backendId)
};
}
function resolveBackendApiKey(backendId?: string): string | undefined {
const backend = getBackend(backendId);
const backendKey = backend?.apiKey?.trim();
if (backendKey) return backendKey;
// an external backend without its own key must not receive the local key
if (backend && backend.id !== LOCAL_BACKEND_ID) return undefined;
return settingsStore.config.apiKey?.toString().trim() || undefined;
}
/**
* Sanitize HTTP headers by redacting sensitive values.
* Known sensitive headers (from HEADERS.REDACTED) and any extra headers
+2 -2
View File
@@ -1,9 +1,9 @@
import { error } from '@sveltejs/kit';
import { browser } from '$app/environment';
import { base } from '$app/paths';
import { HEADERS } from '$lib/constants';
import { MimeTypeApplication } from '$lib/enums';
import { settingsStore } from '$lib/stores/settings/index.svelte';
import { apiUrl } from '$lib/utils/api-base';
/**
* Validates API key by making a request to the server props endpoint
@@ -28,7 +28,7 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise<vo
headers[HEADERS.AUTHORIZATION] = `${HEADERS.BEARER}${apiKey}`;
}
const response = await fetch(`${base}/props`, { headers });
const response = await fetch(apiUrl('/props'), { headers });
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
+2 -2
View File
@@ -2,7 +2,7 @@
* CORS Proxy utility for routing requests through llama-server's CORS proxy.
*/
import { base } from '$app/paths';
import { apiUrl } from './api-base';
import { CORS_PROXY, CORS_PROXY_ENDPOINT } from '$lib/constants';
/**
@@ -11,7 +11,7 @@ import { CORS_PROXY, CORS_PROXY_ENDPOINT } from '$lib/constants';
* @returns URL pointing to the CORS proxy with target encoded
*/
export function buildProxiedUrl(targetUrl: string): URL {
const proxyPath = `${base}${CORS_PROXY_ENDPOINT}`;
const proxyPath = apiUrl(CORS_PROXY_ENDPOINT);
const proxyUrl = new URL(proxyPath, window.location.origin);
proxyUrl.searchParams.set(CORS_PROXY.URL_PARAM, targetUrl);
+2 -2
View File
@@ -2,7 +2,6 @@
import '../app.css';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { SidebarNavigation } from '$lib/components/app';
import { PwaMetaTags, PwaRefreshAlert } from '$lib/components/pwa';
@@ -31,6 +30,7 @@
versionStore
} from '$lib/stores';
import { initStores } from '$lib/stores/init';
import { apiUrl } from '$lib/utils/api-base';
import { ModeWatcher } from 'mode-watcher';
import { untrack } from 'svelte';
import { onMount } from 'svelte';
@@ -167,7 +167,7 @@
[HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey.trim()}`
};
fetch(`${base}/props`, { headers })
fetch(apiUrl('/props'), { headers })
.then((response) => {
if (response.status === 401 || response.status === 403) {
window.location.reload();