Docker model runner download from UI (#4884)
* Enable downloads of DMR models from UI * add utils + dev build * linting
This commit is contained in:
parent
b0672036f2
commit
af998ee0a7
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
@ -6,7 +6,7 @@ concurrency:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['model-table-component'] # put your current branch to create a build. Core team only.
|
branches: ['docker-model-runner-download-from-ui'] # put your current branch to create a build. Core team only.
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'cloud-deployments/*'
|
- 'cloud-deployments/*'
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import { Link } from "react-router-dom";
|
|||||||
import ModelTable from "@/components/lib/ModelTable";
|
import ModelTable from "@/components/lib/ModelTable";
|
||||||
import ModelTableLayout from "@/components/lib/ModelTable/layout";
|
import ModelTableLayout from "@/components/lib/ModelTable/layout";
|
||||||
import ModelTableLoadingSkeleton from "@/components/lib/ModelTable/loading";
|
import ModelTableLoadingSkeleton from "@/components/lib/ModelTable/loading";
|
||||||
|
import DMRUtils from "@/models/utils/dmrUtils";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
|
||||||
export default function DockerModelRunnerOptions({ settings }) {
|
export default function DockerModelRunnerOptions({ settings }) {
|
||||||
const {
|
const {
|
||||||
@ -237,12 +239,44 @@ function DockerModelRunnerModelSelection({
|
|||||||
setFilteredModels(Array.from(filteredModels.values()));
|
setFilteredModels(Array.from(filteredModels.values()));
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
function downloadModel(modelId, _fileSize, progressCallback) {
|
async function downloadModel(modelId, fileSize, progressCallback) {
|
||||||
const [name, tag] = modelId.split(":");
|
try {
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
`Are you sure you want to download this model? It is ${fileSize} in size and may take a while to download.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const { success, error } = await DMRUtils.downloadModel(
|
||||||
|
modelId,
|
||||||
|
basePath,
|
||||||
|
progressCallback
|
||||||
|
);
|
||||||
|
if (!success)
|
||||||
|
throw new Error(
|
||||||
|
error || "An error occurred while downloading the model"
|
||||||
|
);
|
||||||
|
progressCallback(100);
|
||||||
|
handleSetActiveModel(modelId);
|
||||||
|
|
||||||
// Open the model in the Docker Hub (via browser since they may not be installed locally)
|
const existingModels = [...customModels];
|
||||||
window.open(`https://hub.docker.com/layers/${name}/${tag}`, "_blank");
|
const newModel = existingModels.find((model) => model.id === modelId);
|
||||||
progressCallback(100);
|
if (newModel) {
|
||||||
|
newModel.downloaded = true;
|
||||||
|
setCustomModels(existingModels);
|
||||||
|
setFilteredModels(existingModels);
|
||||||
|
setSearchQuery("");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error downloading model:", e);
|
||||||
|
showToast(
|
||||||
|
e.message || "An error occurred while downloading the model",
|
||||||
|
"error",
|
||||||
|
{ clear: true }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupModelsByAlias(models) {
|
function groupModelsByAlias(models) {
|
||||||
|
|||||||
77
frontend/src/models/utils/dmrUtils.js
Normal file
77
frontend/src/models/utils/dmrUtils.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { API_BASE } from "@/utils/constants";
|
||||||
|
import { baseHeaders } from "@/utils/request";
|
||||||
|
import { safeJsonParse } from "@/utils/request";
|
||||||
|
|
||||||
|
const DMRUtils = {
|
||||||
|
/**
|
||||||
|
* Download a DMR model.
|
||||||
|
* @param {string} modelId - The ID of the model to download.
|
||||||
|
* @param {(percentage: number) => void} progressCallback - The callback to receive the progress percentage. If the model is already downloaded, it will be called once with 100.
|
||||||
|
* @returns {Promise<{success: boolean, error: string|null}>}
|
||||||
|
*/
|
||||||
|
downloadModel: async function (
|
||||||
|
modelId,
|
||||||
|
basePath = "",
|
||||||
|
progressCallback = () => {}
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/utils/dmr/download-model`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: baseHeaders(),
|
||||||
|
body: JSON.stringify({ modelId, basePath }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error("Error downloading model: " + response.statusText);
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
const { value, done: readerDone } = await reader.read();
|
||||||
|
if (readerDone) {
|
||||||
|
done = true;
|
||||||
|
resolve({ success: true });
|
||||||
|
} else {
|
||||||
|
const chunk = new TextDecoder("utf-8").decode(value);
|
||||||
|
const lines = chunk.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("data:")) {
|
||||||
|
const data = safeJsonParse(line.slice(5));
|
||||||
|
switch (data?.type) {
|
||||||
|
case "success":
|
||||||
|
done = true;
|
||||||
|
resolve({ success: true });
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
done = true;
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: data?.error || data?.message,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "progress":
|
||||||
|
progressCallback(data?.percentage);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error downloading model:", error);
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
error?.message || "An error occurred while downloading the model",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// Uninstall a DMR model is not supported via the API
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DMRUtils;
|
||||||
@ -21,6 +21,11 @@ function utilEndpoints(app) {
|
|||||||
response.sendStatus(500).end();
|
response.sendStatus(500).end();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
dockerModelRunnerUtilsEndpoints,
|
||||||
|
} = require("./utils/dockerModelRunnerUtils");
|
||||||
|
dockerModelRunnerUtilsEndpoints(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGitVersion() {
|
function getGitVersion() {
|
||||||
|
|||||||
110
server/endpoints/utils/dockerModelRunnerUtils.js
Normal file
110
server/endpoints/utils/dockerModelRunnerUtils.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
const { validatedRequest } = require("../../utils/middleware/validatedRequest");
|
||||||
|
const {
|
||||||
|
flexUserRoleValid,
|
||||||
|
ROLES,
|
||||||
|
} = require("../../utils/middleware/multiUserProtected");
|
||||||
|
const { reqBody } = require("../../utils/http");
|
||||||
|
const { safeJsonParse } = require("../../utils/http");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode HTML entities from a string.
|
||||||
|
* The DMR response is encoded with HTML entities, so we need to decode them
|
||||||
|
* so we can parse the JSON and report the progress percentage.
|
||||||
|
* @param {string} str - The string to decode.
|
||||||
|
* @returns {string} The decoded string.
|
||||||
|
*/
|
||||||
|
function decodeHtmlEntities(str) {
|
||||||
|
return str
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/&/g, "&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dockerModelRunnerUtilsEndpoints(app) {
|
||||||
|
if (!app) return;
|
||||||
|
const {
|
||||||
|
parseDockerModelRunnerEndpoint,
|
||||||
|
} = require("../../utils/AiProviders/dockerModelRunner");
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/utils/dmr/download-model",
|
||||||
|
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
||||||
|
async (request, response) => {
|
||||||
|
try {
|
||||||
|
const { modelId, basePath = "" } = reqBody(request);
|
||||||
|
const dmrUrl = new URL(
|
||||||
|
parseDockerModelRunnerEndpoint(
|
||||||
|
basePath ?? process.env.DOCKER_MODEL_RUNNER_BASE_PATH,
|
||||||
|
"dmr"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
dmrUrl.pathname = "/models/create";
|
||||||
|
response.writeHead(200, {
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
});
|
||||||
|
|
||||||
|
const dmrResponse = await fetch(dmrUrl.toString(), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ from: String(modelId) }),
|
||||||
|
});
|
||||||
|
if (!dmrResponse.ok)
|
||||||
|
throw new Error(
|
||||||
|
dmrResponse.statusText ||
|
||||||
|
"An error occurred while downloading the model"
|
||||||
|
);
|
||||||
|
const reader = dmrResponse.body.getReader();
|
||||||
|
let done = false;
|
||||||
|
while (!done) {
|
||||||
|
const { value, done: readerDone } = await reader.read();
|
||||||
|
if (readerDone) done = true;
|
||||||
|
|
||||||
|
const chunk = new TextDecoder("utf-8").decode(value);
|
||||||
|
const lines = chunk.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
const decodedLine = decodeHtmlEntities(line);
|
||||||
|
const data = safeJsonParse(decodedLine);
|
||||||
|
if (!data) continue;
|
||||||
|
|
||||||
|
if (data.type === "error") {
|
||||||
|
throw new Error(
|
||||||
|
data.message || "An error occurred while downloading the model"
|
||||||
|
);
|
||||||
|
} else if (data.type === "success") {
|
||||||
|
response.write(
|
||||||
|
`data: ${JSON.stringify({ type: "success", percentage: 100, message: "Model downloaded successfully" })}\n\n`
|
||||||
|
);
|
||||||
|
done = true;
|
||||||
|
} else if (data.type === "progress") {
|
||||||
|
const percentage =
|
||||||
|
data.total > 0
|
||||||
|
? Math.round((data.pulled / data.total) * 100)
|
||||||
|
: 0;
|
||||||
|
response.write(
|
||||||
|
`data: ${JSON.stringify({ type: "progress", percentage, message: data.message })}\n\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
response.write(
|
||||||
|
`data: ${JSON.stringify({ type: "error", message: e.message })}\n\n`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
response.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dockerModelRunnerUtilsEndpoints,
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user