| ```mermaid | |
| sequenceDiagram | |
| participant UI as π§© ModelsSelector | |
| participant Hooks as πͺ useModelChangeValidation | |
| participant modelsStore as ποΈ modelsStore | |
| participant serverStore as ποΈ serverStore | |
| participant convStore as ποΈ conversationsStore | |
| participant ModelsSvc as βοΈ ModelsService | |
| participant PropsSvc as βοΈ PropsService | |
| participant API as π llama-server | |
| Note over modelsStore: State:<br/>models: ModelOption[]<br/>routerModels: ApiModelDataEntry[]<br/>selectedModelId, selectedModelName<br/>loading, updating, error<br/>modelLoadingStates (Map)<br/>modelPropsCache (Map)<br/>propsCacheVersion | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Note over UI,API: π INITIALIZATION (MODEL mode) | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| UI->>modelsStore: fetch() | |
| activate modelsStore | |
| modelsStore->>modelsStore: loading = true | |
| alt serverStore.props not loaded | |
| modelsStore->>serverStore: fetch() | |
| Note over serverStore: β see server-flow.mmd | |
| end | |
| modelsStore->>ModelsSvc: list() | |
| ModelsSvc->>API: GET /v1/models | |
| API-->>ModelsSvc: ApiModelListResponse {data: [model]} | |
| modelsStore->>modelsStore: models = $state(mapped) | |
| Note right of modelsStore: Map to ModelOption[]:<br/>{id, name, model, description, capabilities} | |
| Note over modelsStore: MODEL mode: Get modalities from serverStore.props | |
| modelsStore->>modelsStore: modelPropsCache.set(model.id, serverStore.props) | |
| modelsStore->>modelsStore: models[0].modalities = props.modalities | |
| modelsStore->>modelsStore: Auto-select single model | |
| Note right of modelsStore: selectedModelId = models[0].id | |
| modelsStore->>modelsStore: loading = false | |
| deactivate modelsStore | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Note over UI,API: π INITIALIZATION (ROUTER mode) | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| UI->>modelsStore: fetch() | |
| activate modelsStore | |
| modelsStore->>ModelsSvc: list() | |
| ModelsSvc->>API: GET /v1/models | |
| API-->>ModelsSvc: ApiModelListResponse | |
| modelsStore->>modelsStore: models = $state(mapped) | |
| deactivate modelsStore | |
| Note over UI: After models loaded, layout triggers: | |
| UI->>modelsStore: fetchRouterModels() | |
| activate modelsStore | |
| modelsStore->>ModelsSvc: listRouter() | |
| ModelsSvc->>API: GET /v1/models | |
| API-->>ModelsSvc: ApiRouterModelsListResponse | |
| Note right of API: {data: [{id, status, path, in_cache}]} | |
| modelsStore->>modelsStore: routerModels = $state(data) | |
| modelsStore->>modelsStore: fetchModalitiesForLoadedModels() | |
| loop each model where status === "loaded" | |
| modelsStore->>PropsSvc: fetchForModel(modelId) | |
| PropsSvc->>API: GET /props?model={modelId} | |
| API-->>PropsSvc: ApiLlamaCppServerProps | |
| modelsStore->>modelsStore: modelPropsCache.set(modelId, props) | |
| end | |
| modelsStore->>modelsStore: propsCacheVersion++ | |
| deactivate modelsStore | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Note over UI,API: π MODEL SELECTION (ROUTER mode) | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| UI->>Hooks: useModelChangeValidation({getRequiredModalities, onSuccess?, onValidationFailure?}) | |
| Note over Hooks: Hook configured per-component:<br/>ChatForm: getRequiredModalities = usedModalities<br/>ChatMessage: getRequiredModalities = getModalitiesUpToMessage(msgId) | |
| UI->>Hooks: handleModelChange(modelId, modelName) | |
| activate Hooks | |
| Hooks->>Hooks: previousSelectedModelId = modelsStore.selectedModelId | |
| Hooks->>modelsStore: isModelLoaded(modelName)? | |
| alt model NOT loaded | |
| Hooks->>modelsStore: loadModel(modelName) | |
| Note over modelsStore: β see LOAD MODEL section below | |
| end | |
| Note over Hooks: Always fetch props (from cache or API) | |
| Hooks->>modelsStore: fetchModelProps(modelName) | |
| modelsStore-->>Hooks: props | |
| Hooks->>convStore: getRequiredModalities() | |
| convStore-->>Hooks: {vision, audio} | |
| Hooks->>Hooks: Validate: model.modalities β required? | |
| alt validation PASSED | |
| Hooks->>modelsStore: selectModelById(modelId) | |
| Hooks-->>UI: return true | |
| else validation FAILED | |
| Hooks->>UI: toast.error("Model doesn't support required modalities") | |
| alt model was just loaded | |
| Hooks->>modelsStore: unloadModel(modelName) | |
| end | |
| alt onValidationFailure provided | |
| Hooks->>modelsStore: selectModelById(previousSelectedModelId) | |
| end | |
| Hooks-->>UI: return false | |
| end | |
| deactivate Hooks | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Note over UI,API: β¬οΈ LOAD MODEL (ROUTER mode) | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| modelsStore->>modelsStore: loadModel(modelId) | |
| activate modelsStore | |
| alt already loaded | |
| modelsStore-->>modelsStore: return (no-op) | |
| end | |
| modelsStore->>modelsStore: modelLoadingStates.set(modelId, true) | |
| modelsStore->>ModelsSvc: load(modelId) | |
| ModelsSvc->>API: POST /models/load {model: modelId} | |
| API-->>ModelsSvc: {status: "loading"} | |
| modelsStore->>modelsStore: pollForModelStatus(modelId, LOADED) | |
| loop poll every 500ms (max 60 attempts) | |
| modelsStore->>modelsStore: fetchRouterModels() | |
| modelsStore->>ModelsSvc: listRouter() | |
| ModelsSvc->>API: GET /v1/models | |
| API-->>ModelsSvc: models[] | |
| modelsStore->>modelsStore: getModelStatus(modelId) | |
| alt status === LOADED | |
| Note right of modelsStore: break loop | |
| else status === LOADING | |
| Note right of modelsStore: wait 500ms, continue | |
| end | |
| end | |
| modelsStore->>modelsStore: updateModelModalities(modelId) | |
| modelsStore->>PropsSvc: fetchForModel(modelId) | |
| PropsSvc->>API: GET /props?model={modelId} | |
| API-->>PropsSvc: props with modalities | |
| modelsStore->>modelsStore: modelPropsCache.set(modelId, props) | |
| modelsStore->>modelsStore: propsCacheVersion++ | |
| modelsStore->>modelsStore: modelLoadingStates.set(modelId, false) | |
| deactivate modelsStore | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Note over UI,API: β¬οΈ UNLOAD MODEL (ROUTER mode) | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| modelsStore->>modelsStore: unloadModel(modelId) | |
| activate modelsStore | |
| modelsStore->>modelsStore: modelLoadingStates.set(modelId, true) | |
| modelsStore->>ModelsSvc: unload(modelId) | |
| ModelsSvc->>API: POST /models/unload {model: modelId} | |
| modelsStore->>modelsStore: pollForModelStatus(modelId, UNLOADED) | |
| loop poll until unloaded | |
| modelsStore->>ModelsSvc: listRouter() | |
| ModelsSvc->>API: GET /v1/models | |
| end | |
| modelsStore->>modelsStore: modelLoadingStates.set(modelId, false) | |
| deactivate modelsStore | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Note over UI,API: π COMPUTED GETTERS | |
| %% βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Note over modelsStore: Getters:<br/>- selectedModel: ModelOption | null<br/>- loadedModelIds: string[] (from routerModels)<br/>- loadingModelIds: string[] (from modelLoadingStates)<br/>- singleModelName: string | null (MODEL mode only) | |
| Note over modelsStore: Modality helpers:<br/>- getModelModalities(modelId): {vision, audio}<br/>- modelSupportsVision(modelId): boolean<br/>- modelSupportsAudio(modelId): boolean | |
| ``` | |