Use OpenAI-compatible `/v1/models` endpoint by default (#17689)

* refactor: Data fetching via stores

* chore: update webui build output

* refactor: Use OpenAI compat `/v1/models` endpoint by default to list models

* chore: update webui build output

* chore: update webui build output
This commit is contained in:
Aleksander Grygier 2025-12-03 20:49:09 +01:00 committed by GitHub
parent 41c5e02f42
commit e9f9483464
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 45 additions and 90 deletions

Binary file not shown.

View File

@ -15,7 +15,7 @@ sequenceDiagram
Stores->>DB: load conversations Stores->>DB: load conversations
Stores->>API: GET /props Stores->>API: GET /props
API-->>Stores: {role: "router"} API-->>Stores: {role: "router"}
Stores->>API: GET /models Stores->>API: GET /v1/models
API-->>Stores: models[] with status (loaded/available) API-->>Stores: models[] with status (loaded/available)
loop each loaded model loop each loaded model
Stores->>API: GET /props?model=X Stores->>API: GET /props?model=X
@ -28,7 +28,7 @@ sequenceDiagram
alt model not loaded alt model not loaded
Stores->>API: POST /models/load Stores->>API: POST /models/load
loop poll status loop poll status
Stores->>API: GET /models Stores->>API: GET /v1/models
API-->>Stores: check if loaded API-->>Stores: check if loaded
end end
Stores->>API: GET /props?model=X Stores->>API: GET /props?model=X

View File

@ -56,7 +56,7 @@ sequenceDiagram
UI->>modelsStore: fetchRouterModels() UI->>modelsStore: fetchRouterModels()
activate modelsStore activate modelsStore
modelsStore->>ModelsSvc: listRouter() modelsStore->>ModelsSvc: listRouter()
ModelsSvc->>API: GET /models ModelsSvc->>API: GET /v1/models
API-->>ModelsSvc: ApiRouterModelsListResponse API-->>ModelsSvc: ApiRouterModelsListResponse
Note right of API: {data: [{id, status, path, in_cache}]} Note right of API: {data: [{id, status, path, in_cache}]}
modelsStore->>modelsStore: routerModels = $state(data) modelsStore->>modelsStore: routerModels = $state(data)
@ -132,7 +132,7 @@ sequenceDiagram
loop poll every 500ms (max 60 attempts) loop poll every 500ms (max 60 attempts)
modelsStore->>modelsStore: fetchRouterModels() modelsStore->>modelsStore: fetchRouterModels()
modelsStore->>ModelsSvc: listRouter() modelsStore->>ModelsSvc: listRouter()
ModelsSvc->>API: GET /models ModelsSvc->>API: GET /v1/models
API-->>ModelsSvc: models[] API-->>ModelsSvc: models[]
modelsStore->>modelsStore: getModelStatus(modelId) modelsStore->>modelsStore: getModelStatus(modelId)
alt status === LOADED alt status === LOADED
@ -165,7 +165,7 @@ sequenceDiagram
modelsStore->>modelsStore: pollForModelStatus(modelId, UNLOADED) modelsStore->>modelsStore: pollForModelStatus(modelId, UNLOADED)
loop poll until unloaded loop poll until unloaded
modelsStore->>ModelsSvc: listRouter() modelsStore->>ModelsSvc: listRouter()
ModelsSvc->>API: GET /models ModelsSvc->>API: GET /v1/models
end end
modelsStore->>modelsStore: modelLoadingStates.set(modelId, false) modelsStore->>modelsStore: modelLoadingStates.set(modelId, false)

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { ChatMessage } from '$lib/components/app'; import { ChatMessage } from '$lib/components/app';
import { DatabaseService } from '$lib/services/database';
import { chatStore } from '$lib/stores/chat.svelte'; import { chatStore } from '$lib/stores/chat.svelte';
import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte'; import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
import { getMessageSiblings } from '$lib/utils'; import { getMessageSiblings } from '$lib/utils';
@ -19,7 +18,7 @@
const conversation = activeConversation(); const conversation = activeConversation();
if (conversation) { if (conversation) {
DatabaseService.getConversationMessages(conversation.id).then((messages) => { conversationsStore.getConversationMessages(conversation.id).then((messages) => {
allConversationMessages = messages; allConversationMessages = messages;
}); });
} else { } else {

View File

@ -7,7 +7,6 @@
import { Textarea } from '$lib/components/ui/textarea'; import { Textarea } from '$lib/components/ui/textarea';
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config'; import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
import { settingsStore } from '$lib/stores/settings.svelte'; import { settingsStore } from '$lib/stores/settings.svelte';
import { ParameterSyncService } from '$lib/services/parameter-sync';
import { ChatSettingsParameterSourceIndicator } from '$lib/components/app'; import { ChatSettingsParameterSourceIndicator } from '$lib/components/app';
import type { Component } from 'svelte'; import type { Component } from 'svelte';
@ -22,7 +21,7 @@
// Helper function to get parameter source info for syncable parameters // Helper function to get parameter source info for syncable parameters
function getParameterSourceInfo(key: string) { function getParameterSourceInfo(key: string) {
if (!ParameterSyncService.canSyncParameter(key)) { if (!settingsStore.canSyncParameter(key)) {
return null; return null;
} }

View File

@ -2,9 +2,8 @@
import { Download, Upload } from '@lucide/svelte'; import { Download, Upload } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { DialogConversationSelection } from '$lib/components/app'; import { DialogConversationSelection } from '$lib/components/app';
import { DatabaseService } from '$lib/services/database';
import { createMessageCountMap } from '$lib/utils'; import { createMessageCountMap } from '$lib/utils';
import { conversationsStore } from '$lib/stores/conversations.svelte'; import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
let exportedConversations = $state<DatabaseConversation[]>([]); let exportedConversations = $state<DatabaseConversation[]>([]);
let importedConversations = $state<DatabaseConversation[]>([]); let importedConversations = $state<DatabaseConversation[]>([]);
@ -21,15 +20,15 @@
async function handleExportClick() { async function handleExportClick() {
try { try {
const allConversations = await DatabaseService.getAllConversations(); const allConversations = conversations();
if (allConversations.length === 0) { if (allConversations.length === 0) {
alert('No conversations to export'); alert('No conversations to export');
return; return;
} }
const conversationsWithMessages = await Promise.all( const conversationsWithMessages = await Promise.all(
allConversations.map(async (conv) => { allConversations.map(async (conv: DatabaseConversation) => {
const messages = await DatabaseService.getConversationMessages(conv.id); const messages = await conversationsStore.getConversationMessages(conv.id);
return { conv, messages }; return { conv, messages };
}) })
); );
@ -47,7 +46,7 @@
try { try {
const allData: ExportedConversations = await Promise.all( const allData: ExportedConversations = await Promise.all(
selectedConversations.map(async (conv) => { selectedConversations.map(async (conv) => {
const messages = await DatabaseService.getConversationMessages(conv.id); const messages = await conversationsStore.getConversationMessages(conv.id);
return { conv: $state.snapshot(conv), messages: $state.snapshot(messages) }; return { conv: $state.snapshot(conv), messages: $state.snapshot(messages) };
}) })
); );
@ -135,9 +134,7 @@
.snapshot(fullImportData) .snapshot(fullImportData)
.filter((item) => selectedIds.has(item.conv.id)); .filter((item) => selectedIds.has(item.conv.id));
await DatabaseService.importConversations(selectedData); await conversationsStore.importConversationsData(selectedData);
await conversationsStore.loadConversations();
importedConversations = selectedConversations; importedConversations = selectedConversations;
showImportSummary = true; showImportSummary = true;

View File

@ -3,8 +3,7 @@
import * as Table from '$lib/components/ui/table'; import * as Table from '$lib/components/ui/table';
import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app'; import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app';
import { serverStore } from '$lib/stores/server.svelte'; import { serverStore } from '$lib/stores/server.svelte';
import { modelsStore } from '$lib/stores/models.svelte'; import { modelsStore, modelOptions, modelsLoading } from '$lib/stores/models.svelte';
import { ChatService } from '$lib/services/chat';
import { formatFileSize, formatParameters, formatNumber } from '$lib/utils'; import { formatFileSize, formatParameters, formatNumber } from '$lib/utils';
interface Props { interface Props {
@ -16,38 +15,24 @@
let serverProps = $derived(serverStore.props); let serverProps = $derived(serverStore.props);
let modelName = $derived(modelsStore.singleModelName); let modelName = $derived(modelsStore.singleModelName);
let models = $derived(modelOptions());
let isLoadingModels = $derived(modelsLoading());
// Get the first model for single-model mode display
let firstModel = $derived(models[0] ?? null);
// Get modalities from modelStore using the model ID from the first model // Get modalities from modelStore using the model ID from the first model
// For now it supports only for single-model mode, will be extended with further improvements for multi-model functioanlities
let modalities = $derived.by(() => { let modalities = $derived.by(() => {
if (!modelsData?.data?.[0]?.id) return []; if (!firstModel?.id) return [];
return modelsStore.getModelModalitiesArray(firstModel.id);
return modelsStore.getModelModalitiesArray(modelsData.data[0].id);
}); });
let modelsData = $state<ApiModelListResponse | null>(null); // Ensure models are fetched when dialog opens
let isLoadingModels = $state(false);
// Fetch models data when dialog opens
$effect(() => { $effect(() => {
if (open && !modelsData) { if (open && models.length === 0) {
loadModelsData(); modelsStore.fetch();
} }
}); });
async function loadModelsData() {
isLoadingModels = true;
try {
modelsData = await ChatService.getModels();
} catch (error) {
console.error('Failed to load models data:', error);
// Set empty data to prevent infinite loading
modelsData = { object: 'list', data: [] };
} finally {
isLoadingModels = false;
}
}
</script> </script>
<Dialog.Root bind:open {onOpenChange}> <Dialog.Root bind:open {onOpenChange}>
@ -70,8 +55,8 @@
<div class="flex items-center justify-center py-8"> <div class="flex items-center justify-center py-8">
<div class="text-sm text-muted-foreground">Loading model information...</div> <div class="text-sm text-muted-foreground">Loading model information...</div>
</div> </div>
{:else if modelsData && modelsData.data.length > 0} {:else if firstModel}
{@const modelMeta = modelsData.data[0].meta} {@const modelMeta = firstModel.meta}
{#if serverProps} {#if serverProps}
<Table.Root> <Table.Root>

View File

@ -677,48 +677,6 @@ export class ChatService {
// Utilities // Utilities
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
/**
* Get server properties - static method for API compatibility (to be refactored)
*/
static async getServerProps(): Promise<ApiLlamaCppServerProps> {
try {
const response = await fetch(`./props`, {
headers: getJsonHeaders()
});
if (!response.ok) {
throw new Error(`Failed to fetch server props: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching server props:', error);
throw error;
}
}
/**
* Get model information from /models endpoint (to be refactored)
*/
static async getModels(): Promise<ApiModelListResponse> {
try {
const response = await fetch(`./models`, {
headers: getJsonHeaders()
});
if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching models:', error);
throw error;
}
}
/** /**
* Injects a system message at the beginning of the conversation if provided. * Injects a system message at the beginning of the conversation if provided.
* Checks for existing system messages to avoid duplication. * Checks for existing system messages to avoid duplication.

View File

@ -7,7 +7,7 @@ import { getJsonHeaders } from '$lib/utils';
* *
* This service handles communication with model-related endpoints: * This service handles communication with model-related endpoints:
* - `/v1/models` - OpenAI-compatible model list (MODEL + ROUTER mode) * - `/v1/models` - OpenAI-compatible model list (MODEL + ROUTER mode)
* - `/models` - Router-specific model management (ROUTER mode only) * - `/models/load`, `/models/unload` - Router-specific model management (ROUTER mode only)
* *
* **Responsibilities:** * **Responsibilities:**
* - List available models * - List available models
@ -43,7 +43,7 @@ export class ModelsService {
* Returns models with load status, paths, and other metadata * Returns models with load status, paths, and other metadata
*/ */
static async listRouter(): Promise<ApiRouterModelsListResponse> { static async listRouter(): Promise<ApiRouterModelsListResponse> {
const response = await fetch(`${base}/models`, { const response = await fetch(`${base}/v1/models`, {
headers: getJsonHeaders() headers: getJsonHeaders()
}); });

View File

@ -519,6 +519,19 @@ class ConversationsStore {
return await DatabaseService.getConversationMessages(convId); return await DatabaseService.getConversationMessages(convId);
} }
/**
* Imports conversations from provided data (without file picker)
* @param data - Array of conversation data with messages
* @returns Import result with counts
*/
async importConversationsData(
data: ExportedConversations
): Promise<{ imported: number; skipped: number }> {
const result = await DatabaseService.importConversations(data);
await this.loadConversations();
return result;
}
/** /**
* Adds a message to the active messages array * Adds a message to the active messages array
* Used by chatStore when creating new messages * Used by chatStore when creating new messages

View File

@ -370,6 +370,10 @@ class SettingsStore {
return { ...this.config }; return { ...this.config };
} }
canSyncParameter(key: string): boolean {
return ParameterSyncService.canSyncParameter(key);
}
/** /**
* Get parameter information including source for a specific parameter * Get parameter information including source for a specific parameter
*/ */