char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
cae63579b
char/aliceai-t5-moe
1/** 2* ModelsService - Stateless model management API layer 3* 4* Wraps the /models endpoints (list, load, unload) and the /models/sse 5* status feed in MODEL and ROUTER modes. No reactive state; consumed by 6* modelsStore and its status manager. 7*/ 8 9import { base } from '$app/paths' ; 10import { API_MODELS , MODEL_ID } from '$lib/constants' ; 11import { ServerModelStatus } from '$lib/enums' ; 12import type { ParsedModelId } from '$lib/types/models' ; 13import { 14apiFetch , 15apiPost , 16extractSseDataPayload , 17normalizeModelName , 18splitSseRecords 19} from '$lib/utils' ; 20import { getAuthHeaders } from '$lib/utils/api-headers' ; 21 22export class ModelsService { 23private static readonly SSE_RECONNECT_MS = 1000 ; 24 25/** 26* Check if a model is loaded based on its metadata. 27* 28* @param model - Model data entry from the API response 29* @returns True if the model status is LOADED 30*/ 31static isModelLoaded ( model :ApiModelDataEntry ) :boolean { 32return model . status . value === ServerModelStatus . LOADED ; 33} 34 35/** 36* 37* 38* Load/Unload 39* 40* 41*/ 42 43/** 44* Check if a model is currently loading. 45* 46* @param model - Model data entry from the API response 47* @returns True if the model status is LOADING 48*/ 49static isModelLoading ( model :ApiModelDataEntry ) :boolean { 50return model . status . value === ServerModelStatus . LOADING ; 51} 52 53/** 54* Fetch list of models from OpenAI-compatible endpoint. 55* Works in both MODEL and ROUTER modes. 56* 57* @returns List of available models with basic metadata 58*/ 59static async list () :Promise < ApiModelListResponse > { 60return apiFetch < ApiModelListResponse >( API_MODELS . LIST ); 61} 62 63/** 64* Fetch list of all models with detailed metadata (ROUTER mode). 65* Returns models with load status, paths, and other metadata 66* beyond what the OpenAI-compatible endpoint provides. 67* 68* @returns List of models with detailed status and configuration info 69*/ 70static async listRouter () :Promise < ApiRouterModelsListResponse > { 71return apiFetch < ApiRouterModelsListResponse >( API_MODELS . LIST ); 72} 73 74/** 75* Load a model (ROUTER mode only). 76* Sends POST request to `/models/load`. Note: the endpoint returns success 77* before loading completes — use polling to await actual load status. 78* 79* @param modelId - Model identifier to load 80* @param extraArgs - Optional additional arguments to pass to the model instance 81* @returns Load response from the server 82*/ 83static async load ( modelId :string , extraArgs ?:string []) :Promise < ApiRouterModelsLoadResponse > { 84const payload :{ model :string ; extra_args ?:string [] } = { model :modelId }; 85 86if ( extraArgs && extraArgs . length > 0 ) { 87payload . extra_args = extraArgs ; 88} 89 90return apiPost < ApiRouterModelsLoadResponse >( API_MODELS . LOAD , payload ); 91} 92 93/** 94* Parse a model ID string into its structured components. 95* 96* Handles conventions like: 97* `<org>/<ModelName>-<Parameters>(-<ActivatedParameters>)(-<Tags>)(-<Quantization>):<Quantization>` 98* `<ModelName>.<Quantization>` (dot-separated quantization, e.g. `model.Q4_K_M`) 99* 100* @param modelId - Raw model identifier string 101* @returns Structured {@link ParsedModelId} with all detected fields 102*/ 103static parseModelId ( modelId :string ) :ParsedModelId { 104const result :ParsedModelId = { 105activatedParams :null , 106modelName :null , 107orgName :null , 108params :null , 109quantization :null , 110raw :modelId , 111tags :[] 112}; 113// strip directory path and weight extension so a bare `-m /path/file.gguf` 114// parses like a clean repo id; the HF `org/model` form is preserved 115const source = normalizeModelName ( modelId ). replace ( MODEL_ID . WEIGHT_EXTENSION_RE , '' ); 116// 1. Extract colon-separated quantization (e.g. `model:Q4_K_M`) 117const colonIdx = source . indexOf ( MODEL_ID . QUANTIZATION_SEPARATOR ); 118 119let modelPath :string ; 120 121if ( colonIdx !== MODEL_ID . NOT_FOUND ) { 122result . quantization = source . slice ( colonIdx + 1 ) || null ; 123modelPath = source . slice ( 0 , colonIdx ); 124} else { 125modelPath = source ; 126} 127 128// 2. Extract org name (e.g. `org/model` -> org = "org") 129const slashIdx = modelPath . indexOf ( MODEL_ID . ORG_SEPARATOR ); 130 131let modelStr :string ; 132 133if ( slashIdx !== MODEL_ID . NOT_FOUND ) { 134result . orgName = modelPath . slice ( 0 , slashIdx ); 135modelStr = modelPath . slice ( slashIdx + 1 ); 136} else { 137modelStr = modelPath ; 138} 139 140// 3. Handle dot-separated quantization (e.g. `model-name.Q4_K_M`) 141const dotIdx = modelStr . lastIndexOf ( '.' ); 142 143if ( dotIdx !== MODEL_ID . NOT_FOUND && ! result . quantization ) { 144const afterDot = modelStr . slice ( dotIdx + 1 ); 145 146if ( MODEL_ID . QUANTIZATION_SEGMENT_RE . test ( afterDot )) { 147result . quantization = afterDot ; 148modelStr = modelStr . slice ( 0 , dotIdx ); 149} 150} 151 152const segments = modelStr . split ( MODEL_ID . SEGMENT_SEPARATOR ); 153 154// 4. Detect trailing quantization from dash-separated segments 155// Handle UD-prefixed quantization (e.g. `UD-Q8_K_XL`) and 156// standalone quantization (e.g. `Q4_K_M`, `BF16`, `F16`, `MXFP4`) 157if ( ! result . quantization && segments . length > 1 ) { 158const last = segments [ segments . length - 1 ]; 159const secondLast = segments . length > 2 ?segments [ segments . length - 2 ] :null ; 160 161if ( MODEL_ID . QUANTIZATION_SEGMENT_RE . test ( last )) { 162if ( secondLast && MODEL_ID . CUSTOM_QUANTIZATION_PREFIX_RE . test ( secondLast )) { 163result . quantization = ` ${ secondLast } - ${ last } ` ; 164segments . splice ( segments . length - 2 , 2 ); 165} else { 166result . quantization = last ; 167segments . pop (); 168} 169} 170} 171 172// 5. Find params and activated params 173let paramsIdx = MODEL_ID . NOT_FOUND ; 174let activatedParamsIdx = MODEL_ID . NOT_FOUND ; 175 176for ( let i = 0 ; i < segments . length ; i ++ ) { 177const seg = segments [ i ]; 178 179if ( paramsIdx === MODEL_ID . NOT_FOUND && MODEL_ID . PARAMS_RE . test ( seg )) { 180paramsIdx = i ; 181result . params = seg . toUpperCase (); 182} else if ( paramsIdx !== MODEL_ID . NOT_FOUND && MODEL_ID . ACTIVATED_PARAMS_RE . test ( seg )) { 183activatedParamsIdx = i ; 184result . activatedParams = seg . toUpperCase (); 185} 186} 187 188// 6. Model name = segments before params; tags = remaining segments after params 189const pivotIdx = paramsIdx !== MODEL_ID . NOT_FOUND ?paramsIdx :segments . length ; 190const modelSegments = segments . slice ( 0 , pivotIdx ); 191 192// strip trailing container-format segments (e.g. GGUF) from the model name 193while ( 194modelSegments . length > 0 && 195MODEL_ID . IGNORED_SEGMENTS . has ( modelSegments [ modelSegments . length - 1 ]. toUpperCase ()) 196) { 197modelSegments . pop (); 198} 199 200result . modelName = modelSegments . join ( MODEL_ID . SEGMENT_SEPARATOR ) || null ; 201 202if ( paramsIdx !== MODEL_ID . NOT_FOUND ) { 203result . tags = segments . slice ( paramsIdx + 1 ). filter (( _ , relIdx ) => { 204const absIdx = paramsIdx + 1 + relIdx ; 205 206if ( absIdx === activatedParamsIdx ) return false ; 207 208return ! MODEL_ID . IGNORED_SEGMENTS . has ( segments [ absIdx ]. toUpperCase ()); 209}); 210} 211 212return result ; 213} 214 215/** 216* Unload a model (ROUTER mode only). 217* Sends POST request to `/models/unload`. Note: the endpoint returns success 218* before unloading completes — use polling to await actual unload status. 219* 220* @param modelId - Model identifier to unload 221* @returns Unload response from the server 222*/ 223static async unload ( modelId :string ) :Promise < ApiRouterModelsUnloadResponse > { 224return apiPost < ApiRouterModelsUnloadResponse >( API_MODELS . UNLOAD , { model :modelId }); 225} 226 227/** 228* Read the /models/sse feed and invoke onEvent for each parsed envelope. 229* Reconnects on network drops until the signal aborts. Splits the byte 230* stream into SSE records on the blank line boundary; the payload rides in 231* the data lines as a JSON envelope with its own model, event and data fields. 232*/ 233static async watchModelEvents ( 234signal :AbortSignal , 235onEvent :( event :ApiModelsSseEvent ) => void 236) :Promise < void > { 237const decoder = new TextDecoder (); 238 239while ( ! signal . aborted ) { 240try { 241const response = await fetch ( ` ${ base } ${ API_MODELS . SSE } ` , { 242headers :getAuthHeaders (), 243 signal 244}); 245 246if ( response . ok && response . body ) { 247const reader = response . body . getReader (); 248 249let buffer = '' ; 250 251while ( ! signal . aborted ) { 252const { done, value} = await reader . read (); 253 254if ( done ) break ; 255 256buffer += decoder . decode ( value , { stream :true }); 257 258const { records, rest} = splitSseRecords ( buffer ); 259 260buffer = rest ; 261 262for ( const record of records ) { 263const event = ModelsService . parseStatusRecord ( record ); 264 265if ( event ) onEvent ( event ); 266} 267} 268} 269} catch { 270// network drop or abort falls through to the reconnect delay 271} 272 273if ( signal . aborted ) return ; 274 275await new Promise (( resolve ) => setTimeout ( resolve , ModelsService . SSE_RECONNECT_MS )); 276} 277} 278 279/** 280* Parse one SSE record into its JSON envelope, or null when the record 281* carries no data payload or malformed JSON. 282*/ 283private static parseStatusRecord ( record :string ) :ApiModelsSseEvent | null { 284const payload = extractSseDataPayload ( record ); 285 286if ( payload . length === 0 ) return null ; 287 288try { 289return JSON . parse ( payload ) as ApiModelsSseEvent ; 290} catch { 291return null ; 292} 293} 294}