Skip to content

Conversation

@mostlygeek
Copy link
Owner

@mostlygeek mostlygeek commented Sep 24, 2025

  • Add new api endpoint: /unload/:model_id to unload a specific model.
  • Add button to UI to unload a single model:
image

Fixes #312

Summary by CodeRabbit

  • New Features

    • Unload a single model without affecting others via a new backend endpoint and UI control.
    • Models list shows Load for stopped models and an Unload button for active models (enabled when ready).
  • Tests

    • Added coverage for single-model unload.
    • Improved test reliability by waiting for shutdown signals instead of fixed sleeps.
  • Chores

    • Development proxy updated to route /unload to the backend.
  • Documentation

    • Public API extended to include per-model unload for UI consumers.

@coderabbitai
Copy link

coderabbitai bot commented Sep 24, 2025

Walkthrough

Adds API and UI support to unload a single model: new ProcessGroup.StopProcess(modelID, strategy), a /api/models/unload/*model handler, tests updated to wait on process shutdown signals, UI API method unloadSingleModel and per-model unload controls, plus a dev proxy entry.

Changes

Cohort / File(s) Summary of changes
Process management (core)
proxy/processgroup.go
Added func (pg *ProcessGroup) StopProcess(modelID string, strategy StopStrategy) error — looks up a model in the group, clears lastUsedProcess if matching, and calls StopImmediately or Stop based on strategy.
Proxy manager API (routes/handlers)
proxy/proxymanager_api.go
Added route /api/models/unload/*model and handler apiUnloadSingleModelHandler which resolves the real model name, finds its ProcessGroup, and stops the process (returns appropriate HTTP errors on failures).
Proxy manager runtime
proxy/proxymanager.go
Minor whitespace cleanup (removed extra blank line in setupGinEngine).
Proxy manager tests
proxy/proxymanager_test.go
Replaced fixed sleep with select-based wait on cmdWaitChan (2s timeout); added TestProxyManager_UnloadSingleModel to unload one model and assert it reaches StateStopped; updated TestProxyManager_Unload to use channel wait.
UI API provider
ui/src/contexts/APIProvider.tsx
Added unloadSingleModel(model: string): Promise<void> which POSTs to /api/models/unload/{model}, throws on non-OK responses, and is exposed in the provider value; unloadAllModels adjusted to /api/models/unload.
UI Models page
ui/src/pages/Models.tsx
Integrated unloadSingleModel into per-row actions: show Load when model is "stopped", otherwise show Unload (enabled only when state is "ready"); updated icons/labels for unload-all action.
UI dev proxy config
ui/vite.config.ts
Added dev proxy mapping "/unload": "http://localhost:8080" to forward unload requests to backend during development.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant UI as Models Page
  participant API as APIProvider
  participant S as /api/models/unload/*model
  participant PM as ProxyManager
  participant PG as ProcessGroup
  participant P as Process

  U->>UI: Click "Unload" on model
  UI->>API: unloadSingleModel(modelId)
  API->>S: POST /api/models/unload/{modelId}
  S->>PM: apiUnloadSingleModelHandler (resolve real name)
  PM->>PG: find group for model
  PM->>PG: StopProcess(realModelName, strategy)
  PG->>P: StopImmediately / Stop (per strategy)
  P-->>PG: cmdWaitChan signals exit
  PG-->>PM: return nil
  PM-->>S: 200 OK
  S-->>API: 200 OK
  API-->>UI: resolved Promise
  UI-->>U: UI updates model state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly describes the main change by highlighting the addition of single-model unload capability, matching the PR’s objectives and clearly informing readers of its intent.
Linked Issues Check ✅ Passed The implementation fulfills the linked issue’s requirements by adding the /api/models/unload/:model endpoint with corresponding backend stop logic, exposing unloadSingleModel in the API client, updating the UI to render per-model Unload buttons and a renamed “Unload All” control, and covering the new functionality with tests against isolated model unloading as specified in issue #312.
Out of Scope Changes Check ✅ Passed All code modifications directly support the new per-model unload feature or related testing and developer convenience; the only change not impacting functionality is a benign whitespace adjustment in proxymanager.go.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch unload-single-process

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/src/pages/Models.tsx (1)

88-95: Rename the top-level control to “Unload All” to meet the PR objective

The objective calls for explicitly labeling the global action.

Apply this diff:

-            <RiStopCircleLine size="24" /> {isUnloading ? "Unloading..." : "Unload"}
+            <RiStopCircleLine size="24" /> {isUnloading ? "Unloading..." : "Unload All"}
🧹 Nitpick comments (7)
ui/vite.config.ts (1)

18-18: Dev proxy addition looks good

Proxying /unload to the backend aligns with the new endpoint.

For consistency, consider converging state‑changing routes under /api (e.g., POST /api/models/:id/unload) rather than mixing /api and root paths with GET.

proxy/processgroup.go (1)

89-100: Don’t hold the group lock while stopping a process

Holding pg.Lock during StopImmediately can block request paths (e.g., swap lock in ProxyRequest). Unlock before stopping; optionally clear lastUsedProcess if it matches.

Apply this diff:

 func (pg *ProcessGroup) StopProcess(modelID string) error {
-	pg.Lock()
-	defer pg.Unlock()
-
-	process, exists := pg.processes[modelID]
-	if !exists {
-		return fmt.Errorf("process not found for %s", modelID)
-	}
-
-	process.StopImmediately()
-	return nil
+	pg.Lock()
+	process, exists := pg.processes[modelID]
+	if !exists {
+		pg.Unlock()
+		return fmt.Errorf("process not found for %s", modelID)
+	}
+	if pg.lastUsedProcess == modelID {
+		pg.lastUsedProcess = ""
+	}
+	pg.Unlock()
+
+	process.StopImmediately()
+	return nil
 }
proxy/proxymanager_test.go (1)

413-457: Also assert non-target models remain loaded

Verify unloading model1 doesn’t affect model2 in the same group.

Apply this diff:

 	assert.Equal(t, proxy.processGroups[testGroupId].processes["model1"].CurrentState(), StateStopped)
+	assert.Equal(t, proxy.processGroups[testGroupId].processes["model2"].CurrentState(), StateReady)
 }
proxy/proxymanager.go (1)

632-653: Use consistent error responses and proper content negotiation

Leverage sendErrorResponse for errors so clients get JSON when Accept: application/json.

Apply this diff:

 func (pm *ProxyManager) unloadSingleModelHandler(c *gin.Context) {
 	requestedModel := strings.TrimPrefix(c.Param("model"), "/")

 	realModelName, found := pm.config.RealModelName(requestedModel)
 	if !found {
-		c.String(http.StatusNotFound, "Model not found")
+		pm.sendErrorResponse(c, http.StatusNotFound, "model not found")
 		return
 	}

 	processGroup := pm.findGroupByModelName(realModelName)
 	if processGroup == nil {
-		c.String(http.StatusInternalServerError, "process group not found for model %s", requestedModel)
+		pm.sendErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("process group not found for model %s", requestedModel))
 		return
 	}

 	if err := processGroup.StopProcess(realModelName); err != nil {
-		c.String(http.StatusInternalServerError, "error stopping process: %s", err.Error())
+		pm.sendErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("error stopping process: %s", err.Error()))
 		return
 	} else {
 		c.String(http.StatusOK, "OK")
 	}
 }

Also consider using a non‑GET method for this mutation (e.g., POST /api/models/:id/unload or DELETE /api/models/:id) for better REST semantics and CSRF safety.

ui/src/contexts/APIProvider.tsx (2)

193-205: Avoid GET for a state-changing operation; align under /api

Prefer a non‑GET verb under /api for consistency with unloadAllModels.

Example (assuming backend support):

await fetch(`/api/models/${model}/unload`, { method: "POST" });

226-226: Include unloadSingleModel (and connectionStatus) in useMemo deps

Prevents stale closures and satisfies exhaustive-deps linting.

Add unloadSingleModel and connectionStatus to the dependency array of useMemo.

ui/src/pages/Models.tsx (1)

122-134: Show “Load” for both stopped and shutdown states

Users likely expect to load from either state.

Apply this diff:

-                  {model.state === "stopped" ? (
+                  {model.state === "stopped" || model.state === "shutdown" ? (
                     <button className="btn btn--sm" onClick={() => loadModel(model.id)}>
                       Load
                     </button>
                   ) : (
                     <button
                       className="btn btn--sm"
                       onClick={() => unloadSingleModel(model.id)}
                       disabled={model.state !== "ready"}
                     >
                       Unload
                     </button>
                   )}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc3bb71 and 2efce5c.

📒 Files selected for processing (6)
  • proxy/processgroup.go (1 hunks)
  • proxy/proxymanager.go (2 hunks)
  • proxy/proxymanager_test.go (1 hunks)
  • ui/src/contexts/APIProvider.tsx (3 hunks)
  • ui/src/pages/Models.tsx (2 hunks)
  • ui/vite.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
proxy/processgroup.go (1)
proxy/process.go (4)
  • StopImmediately (36-36)
  • ID (40-75)
  • p (308-317)
  • p (321-333)
proxy/proxymanager_test.go (3)
proxy/config.go (5)
  • DEFAULT_GROUP_ID (18-18)
  • AddDefaultGroupToConfig (370-412)
  • Config (149-169)
  • ModelConfig (20-40)
  • GroupConfig (116-121)
proxy/process.go (2)
  • StateStopped (24-24)
  • StateReady (26-26)
proxy/proxymanager.go (1)
  • New (47-130)
ui/src/pages/Models.tsx (1)
ui/src/contexts/APIProvider.tsx (1)
  • useAPI (240-246)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-tests
🔇 Additional comments (4)
proxy/proxymanager_test.go (1)

404-409: Deterministic wait on shutdown channel is a solid improvement

Waiting on cmdWaitChan removes timing flakiness vs. fixed sleeps.

ui/src/contexts/APIProvider.tsx (1)

19-19: API surface update is appropriate

Adding unloadSingleModel to the context interface matches UI usage.

ui/src/pages/Models.tsx (1)

40-40: Wiring unloadSingleModel into the panel is correct

Matches the new API and per-model control objective.

proxy/proxymanager.go (1)

232-234: Static routes already take precedence — no reordering needed

Gin (via julienschmidt/httprouter) prioritizes exact/static routes over wildcard/catch‑all routes, so "/unload" will be handled by the static route regardless of registration order. "/unload/*model" matches "/unload/" and deeper paths and the router may perform a trailing‑slash redirect between forms. Leave proxy/proxymanager.go unchanged.

Likely an incorrect or invalid review comment.

@mostlygeek mostlygeek self-assigned this Sep 25, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
ui/src/contexts/APIProvider.tsx (2)

195-203: Safely encode model IDs when building the unload URL.

Line 195 concatenates the raw model string into the path. Any reserved characters (?, #, spaces, etc.) would corrupt the request (e.g., model="foo?bar" turns everything after ? into query parameters), and we already have real model IDs containing path separators. Please URL-encode each path segment so the backend reliably receives the intended identifier without breaking slash semantics.

Apply this diff:

-      const response = await fetch(`/api/models/unload/${model}`, {
+      const encodedModel = model
+        .split("/")
+        .map((segment) => encodeURIComponent(segment))
+        .join("/");
+
+      const response = await fetch(`/api/models/unload/${encodedModel}`, {
         method: "POST",
       });

221-235: Keep the provider’s memoized value aligned with its dependencies.

Line 226 adds unloadSingleModel to the context value, but the dependency array on Line 233 still omits it (and it already missed connectionStatus). If either callback/state ever changes, consumers would keep seeing stale references. Please add both dependencies so the memo stays consistent.

Apply this diff:

-    [models, listModels, unloadAllModels, loadModel, enableAPIEvents, proxyLogs, upstreamLogs, metrics]
+    [
+      models,
+      listModels,
+      unloadAllModels,
+      unloadSingleModel,
+      loadModel,
+      enableAPIEvents,
+      proxyLogs,
+      upstreamLogs,
+      metrics,
+      connectionStatus,
+    ]
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2efce5c and e8f6751.

📒 Files selected for processing (6)
  • proxy/processgroup.go (1 hunks)
  • proxy/proxymanager.go (0 hunks)
  • proxy/proxymanager_api.go (3 hunks)
  • proxy/proxymanager_test.go (1 hunks)
  • ui/src/contexts/APIProvider.tsx (4 hunks)
  • ui/src/pages/Models.tsx (4 hunks)
💤 Files with no reviewable changes (1)
  • proxy/proxymanager.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • proxy/proxymanager_test.go
  • ui/src/pages/Models.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
proxy/proxymanager_api.go (2)
proxy/proxymanager.go (1)
  • ProxyManager (27-45)
proxy/process.go (1)
  • StopImmediately (36-36)
proxy/processgroup.go (1)
proxy/process.go (2)
  • StopStrategy (33-33)
  • StopImmediately (36-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-tests
🔇 Additional comments (1)
proxy/processgroup.go (1)

89-110: LGTM – per-model stop logic is thread-safe and aligns with existing strategies.

Locking, clearing lastUsedProcess, and delegating to the appropriate stop method mirror the all-process path and should behave correctly for both immediate and graceful strategies.

@mostlygeek mostlygeek merged commit 1a84926 into main Sep 25, 2025
3 of 4 checks passed
@mostlygeek mostlygeek deleted the unload-single-process branch September 25, 2025 03:53
@olegshulyakov
Copy link
Contributor

@mostlygeek Suggest to rename top button to "Unload All"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability to Unload Individual Models

3 participants