Initial ForgeLM import
This commit is contained in:
commit
a392b9dc60
4296 files changed
+1212675
No files matched your search
@@ -0,0 +1,446 @@
|
||||
import type {
|
||||
ConnectionMode,
|
||||
RuntimeApplyImpact,
|
||||
RuntimeConfig,
|
||||
RuntimeConnection,
|
||||
RuntimeDraftFieldKey,
|
||||
RuntimeLifecycleCommand,
|
||||
RuntimeLifecycleResult,
|
||||
RuntimeModelFile,
|
||||
RuntimeSnapshot,
|
||||
RuntimeWorkspaceHealth,
|
||||
RuntimeWorkspaceHealthTone,
|
||||
RuntimeWorkspaceSummary,
|
||||
} from "./types";
|
||||
import { scanRuntimeModels } from "./api";
|
||||
|
||||
const DEFAULT_LOCAL_URL = "http://127.0.0.1:8080";
|
||||
|
||||
// === Phase 4: Source-Mode Validation Types ===
|
||||
|
||||
export type ValidationSeverity = "error" | "warning";
|
||||
|
||||
export interface FieldValidation {
|
||||
valid: boolean;
|
||||
severity: ValidationSeverity;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
export interface SourceModeValidation {
|
||||
mode: ConnectionMode;
|
||||
valid: boolean;
|
||||
fieldValidations: Record<string, FieldValidation>;
|
||||
}
|
||||
|
||||
export interface PathValidation {
|
||||
path: string;
|
||||
exists: boolean;
|
||||
isValid: boolean;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
// === Phase 4: Source-Mode Validation Functions ===
|
||||
|
||||
/**
|
||||
* Validates source-mode configuration based on connection mode.
|
||||
* For local mode, requires model_path. For remote mode, requires server_url.
|
||||
*/
|
||||
export function validateSourceMode(config: RuntimeConfig): SourceModeValidation {
|
||||
const fieldValidations: Record<string, FieldValidation> = {};
|
||||
let valid = true;
|
||||
|
||||
if (config.connection_mode === "local") {
|
||||
// Validate local mode requirements
|
||||
if (!config.model_path) {
|
||||
fieldValidations.model_path = {
|
||||
valid: false,
|
||||
severity: "error",
|
||||
message: "Select a model file for local runtime",
|
||||
};
|
||||
valid = false;
|
||||
}
|
||||
// models_dir is optional for local mode
|
||||
} else if (config.connection_mode === "remote") {
|
||||
// Validate remote mode requirements
|
||||
if (!config.server_url) {
|
||||
fieldValidations.server_url = {
|
||||
valid: false,
|
||||
severity: "error",
|
||||
message: "Server URL is required for remote runtime",
|
||||
};
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Validate URL format for remote mode
|
||||
if (config.server_url) {
|
||||
try {
|
||||
const url = new URL(config.server_url);
|
||||
if (!["http:", "https:"].includes(url.protocol)) {
|
||||
fieldValidations.server_url = {
|
||||
valid: false,
|
||||
severity: "error",
|
||||
message: "Server URL must use http or https protocol",
|
||||
};
|
||||
valid = false;
|
||||
}
|
||||
} catch {
|
||||
fieldValidations.server_url = {
|
||||
valid: false,
|
||||
severity: "error",
|
||||
message: "Invalid URL format",
|
||||
};
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mode: config.connection_mode,
|
||||
valid,
|
||||
fieldValidations,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a model path. For now, returns pending status.
|
||||
* Actual existence check happens via scan in plan 04-02.
|
||||
*/
|
||||
export function validateModelPath(path: string | null): PathValidation {
|
||||
if (!path) {
|
||||
return { path: "", exists: false, isValid: false, message: null };
|
||||
}
|
||||
// For now, return valid - actual existence check happens via scan
|
||||
return { path, exists: true, isValid: true, message: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field keys that require validation based on connection mode
|
||||
*/
|
||||
export function getValidatedFields(mode: ConnectionMode): RuntimeDraftFieldKey[] {
|
||||
if (mode === "local") {
|
||||
return ["model_path", "models_dir"];
|
||||
}
|
||||
return ["server_url", "api_key"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if draft has any validation errors
|
||||
*/
|
||||
export function hasValidationErrors(fieldValidations: Record<string, FieldValidation>): boolean {
|
||||
return Object.values(fieldValidations).some((fv) => !fv.valid);
|
||||
}
|
||||
|
||||
// === Phase 4: Path and Model File Validation (Async) ===
|
||||
|
||||
export interface DirectoryValidation {
|
||||
valid: boolean;
|
||||
modelCount: number;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface ModelFileValidation {
|
||||
valid: boolean;
|
||||
exists: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
const RESTART_REQUIRED_FIELDS: Array<keyof RuntimeConfig> = [
|
||||
"connection_mode",
|
||||
"model_path",
|
||||
"addr",
|
||||
"gpu_layers",
|
||||
"ctx_size",
|
||||
];
|
||||
|
||||
function normalizeUrl(value: string | null | undefined): string {
|
||||
const trimmed = (value || "").trim();
|
||||
return trimmed ? trimmed.replace(/\/+$/, "") : "";
|
||||
}
|
||||
|
||||
export function getRuntimeConnection(snapshot: RuntimeSnapshot): RuntimeConnection {
|
||||
if (snapshot.config.connection_mode === "remote") {
|
||||
return {
|
||||
mode: "remote",
|
||||
serverUrl: normalizeUrl(snapshot.config.server_url),
|
||||
apiKey: snapshot.config.api_key || "",
|
||||
managed: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode: "local",
|
||||
serverUrl: normalizeUrl(snapshot.resolved_url) || DEFAULT_LOCAL_URL,
|
||||
apiKey: snapshot.config.api_key || "",
|
||||
managed: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRuntimeStateLabel(snapshot: RuntimeSnapshot): string {
|
||||
if (snapshot.config.connection_mode === "remote") {
|
||||
return "Remote API";
|
||||
}
|
||||
|
||||
switch (snapshot.status) {
|
||||
case "ready":
|
||||
return "Ready";
|
||||
case "starting":
|
||||
return "Starting local API";
|
||||
case "stopping":
|
||||
return "Stopping local API";
|
||||
case "error":
|
||||
return "Local API error";
|
||||
case "degraded":
|
||||
return "Attention needed";
|
||||
default:
|
||||
return "Local API offline";
|
||||
}
|
||||
}
|
||||
|
||||
export function getRuntimeStatusTone(snapshot: RuntimeSnapshot): "neutral" | "success" | "warning" | "danger" {
|
||||
if (snapshot.config.connection_mode === "remote") {
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
switch (snapshot.status) {
|
||||
case "ready":
|
||||
return "success";
|
||||
case "starting":
|
||||
case "stopping":
|
||||
case "degraded":
|
||||
return "warning";
|
||||
case "error":
|
||||
return "danger";
|
||||
default:
|
||||
return "neutral";
|
||||
}
|
||||
}
|
||||
|
||||
export interface RuntimeStartGuard {
|
||||
canStart: boolean;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface RuntimeApplyImpactOptions {
|
||||
savedBaseline: RuntimeConfig;
|
||||
draftConfig: RuntimeConfig;
|
||||
processRunning: boolean;
|
||||
}
|
||||
|
||||
export function createRuntimeLifecycleResult(
|
||||
command: RuntimeLifecycleCommand = "idle",
|
||||
message = "Runtime idle.",
|
||||
outcome: RuntimeLifecycleResult["outcome"] = "succeeded",
|
||||
): RuntimeLifecycleResult {
|
||||
return {
|
||||
command,
|
||||
outcome,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRuntimeApplyImpact({
|
||||
savedBaseline,
|
||||
draftConfig,
|
||||
processRunning,
|
||||
}: RuntimeApplyImpactOptions): RuntimeApplyImpact {
|
||||
if (savedBaseline.connection_mode !== draftConfig.connection_mode) {
|
||||
return "save_before_apply";
|
||||
}
|
||||
|
||||
const needsRestart = RESTART_REQUIRED_FIELDS.some((field) => savedBaseline[field] !== draftConfig[field]);
|
||||
|
||||
if (processRunning && needsRestart) {
|
||||
return "restart_required";
|
||||
}
|
||||
|
||||
return "immediate";
|
||||
}
|
||||
|
||||
export function getRuntimeStartGuard(snapshot: RuntimeSnapshot): RuntimeStartGuard {
|
||||
if (snapshot.config.connection_mode === "remote") {
|
||||
return {
|
||||
canStart: false,
|
||||
reason: "Remote mode uses an external API and does not launch the local runtime.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!snapshot.binary_path) {
|
||||
return {
|
||||
canStart: false,
|
||||
reason: "Build or bundle the local API binary before starting the managed runtime.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!snapshot.config.model_path?.trim()) {
|
||||
return {
|
||||
canStart: false,
|
||||
reason: "Choose a local model before starting the managed runtime.",
|
||||
};
|
||||
}
|
||||
|
||||
return { canStart: true, reason: "" };
|
||||
}
|
||||
|
||||
export function createDefaultRuntimeSnapshot(): RuntimeSnapshot {
|
||||
return {
|
||||
status: "stopped",
|
||||
ready: false,
|
||||
process_running: false,
|
||||
resolved_url: DEFAULT_LOCAL_URL,
|
||||
binary_path: null,
|
||||
last_error: null,
|
||||
active_command: "idle",
|
||||
last_command_result: createRuntimeLifecycleResult(),
|
||||
config: {
|
||||
connection_mode: "local",
|
||||
start_policy: "manual",
|
||||
server_url: "",
|
||||
api_key: "",
|
||||
model_path: null,
|
||||
models_dir: null,
|
||||
addr: "127.0.0.1:8080",
|
||||
gpu_layers: 0,
|
||||
ctx_size: 4096,
|
||||
extra_model_paths: [],
|
||||
selected_model_id: null,
|
||||
remote_presets: [],
|
||||
hugging_face_token: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// === Phase 3: Unified Runtime Workspace Helpers ===
|
||||
|
||||
function getModelLabel(modelPath: string | null): string | null {
|
||||
if (!modelPath) return null;
|
||||
const parts = modelPath.replace(/\\/g, "/").split("/");
|
||||
return parts[parts.length - 1] || null;
|
||||
}
|
||||
|
||||
export function getRuntimeWorkspaceSummary(snapshot: RuntimeSnapshot): RuntimeWorkspaceSummary {
|
||||
const connection = getRuntimeConnection(snapshot);
|
||||
const statusLabel = getRuntimeStateLabel(snapshot);
|
||||
const isRemote = snapshot.config.connection_mode === "remote";
|
||||
|
||||
return {
|
||||
statusLabel,
|
||||
modeLabel: isRemote ? "remote" : "local",
|
||||
endpointLabel: connection.serverUrl,
|
||||
modelLabel: getModelLabel(snapshot.config.model_path),
|
||||
isReady: snapshot.ready,
|
||||
isRunning: snapshot.process_running,
|
||||
hasError: snapshot.status === "error" || (snapshot.last_error !== null && snapshot.last_error.code !== "poll_warning"),
|
||||
};
|
||||
}
|
||||
|
||||
function deriveHealthTone(snapshot: RuntimeSnapshot): RuntimeWorkspaceHealthTone {
|
||||
if (snapshot.config.connection_mode === "remote") {
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
switch (snapshot.status) {
|
||||
case "ready":
|
||||
return "success";
|
||||
case "starting":
|
||||
case "stopping":
|
||||
case "degraded":
|
||||
return "warning";
|
||||
case "error":
|
||||
return "danger";
|
||||
default:
|
||||
return "neutral";
|
||||
}
|
||||
}
|
||||
|
||||
function deriveHealthHeadline(snapshot: RuntimeSnapshot): string {
|
||||
if (snapshot.config.connection_mode === "remote") {
|
||||
return "Remote API connected";
|
||||
}
|
||||
|
||||
switch (snapshot.status) {
|
||||
case "ready":
|
||||
return "Local API ready";
|
||||
case "starting":
|
||||
return "Starting local API...";
|
||||
case "stopping":
|
||||
return "Stopping local API...";
|
||||
case "degraded":
|
||||
return "Attention needed";
|
||||
case "error":
|
||||
return "Local API error";
|
||||
default:
|
||||
return "Local API offline";
|
||||
}
|
||||
}
|
||||
|
||||
function deriveHealthDetail(snapshot: RuntimeSnapshot): string {
|
||||
if (snapshot.config.connection_mode === "remote") {
|
||||
return snapshot.config.server_url || "Configure remote endpoint";
|
||||
}
|
||||
|
||||
if (snapshot.last_error) {
|
||||
return snapshot.last_error.message;
|
||||
}
|
||||
|
||||
if (snapshot.status === "ready" && snapshot.process_running) {
|
||||
return snapshot.resolved_url || "http://127.0.0.1:8080";
|
||||
}
|
||||
|
||||
if (snapshot.status === "stopped") {
|
||||
return "Start the local API to begin";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getRuntimeHealthSummary(snapshot: RuntimeSnapshot): RuntimeWorkspaceHealth {
|
||||
return {
|
||||
headline: deriveHealthHeadline(snapshot),
|
||||
detail: deriveHealthDetail(snapshot),
|
||||
tone: deriveHealthTone(snapshot),
|
||||
};
|
||||
}
|
||||
|
||||
// === Phase 4: Async Path and Model Validation ===
|
||||
|
||||
/**
|
||||
* Validate models directory by scanning it
|
||||
*/
|
||||
export async function validateModelsDirectory(
|
||||
path: string | null,
|
||||
): Promise<DirectoryValidation> {
|
||||
if (!path) {
|
||||
return { valid: false, modelCount: 0, error: "No models directory selected" };
|
||||
}
|
||||
try {
|
||||
const models = await scanRuntimeModels(path);
|
||||
return {
|
||||
valid: true,
|
||||
modelCount: models.length,
|
||||
error: null,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
valid: false,
|
||||
modelCount: 0,
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a model path exists in the scanned models list
|
||||
*/
|
||||
export function validateModelFile(
|
||||
path: string | null,
|
||||
scannedModels: RuntimeModelFile[],
|
||||
): ModelFileValidation {
|
||||
if (!path) {
|
||||
return { valid: false, exists: false, error: "No model selected" };
|
||||
}
|
||||
const exists = scannedModels.some((m) => m.path === path);
|
||||
return {
|
||||
valid: exists,
|
||||
exists,
|
||||
error: exists ? null : "Model file not found in current models directory",
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user