Compare commits
1 commit
main
...
pr-4741-51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81926463d9 |
14 changed files with 3191 additions and 453 deletions
|
|
@ -749,6 +749,7 @@ shell.Run cmd, 0, False
|
|||
} else {
|
||||
step "gpu" "none (chat-only / GGUF)" "Yellow"
|
||||
substep "Training and GPU inference require an NVIDIA GPU with drivers installed." "Yellow"
|
||||
substep "https://www.nvidia.com/Download/index.aspx" "Yellow"
|
||||
}
|
||||
|
||||
# ── Choose the correct PyTorch index URL based on driver CUDA version ──
|
||||
|
|
@ -776,10 +777,10 @@ shell.Run cmd, 0, False
|
|||
# ── Print CPU-only hint when no GPU detected ──
|
||||
if (-not $SkipTorch -and $TorchIndexUrl -like "*/cpu") {
|
||||
Write-Host ""
|
||||
substep "No NVIDIA GPU detected." "Yellow"
|
||||
substep "Installing CPU-only PyTorch. If you only need GGUF chat/inference," "Yellow"
|
||||
substep "re-run with --no-torch for a faster, lighter install:" "Yellow"
|
||||
substep ".\install.ps1 --no-torch" "Yellow"
|
||||
Write-Host " NOTE: No NVIDIA GPU detected." -ForegroundColor Yellow
|
||||
Write-Host " Installing CPU-only PyTorch. If you only need GGUF chat/inference,"
|
||||
Write-Host " re-run with --no-torch for a faster, lighter install:"
|
||||
Write-Host " .\install.ps1 --no-torch"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -121,13 +121,13 @@ async def lifespan(app: FastAPI):
|
|||
if storage.ensure_default_admin():
|
||||
bootstrap_pw = storage.get_bootstrap_password()
|
||||
app.state.bootstrap_password = bootstrap_pw
|
||||
|
||||
bootstrap_path = storage.DB_PATH.parent / ".bootstrap_password"
|
||||
print("\n" + "=" * 60)
|
||||
print("DEFAULT ADMIN ACCOUNT CREATED")
|
||||
print(
|
||||
"Sign in with the seeded credentials and change the password immediately:\n"
|
||||
)
|
||||
print(f" username: {storage.DEFAULT_ADMIN_USERNAME}")
|
||||
print(f" password saved to: {bootstrap_path}")
|
||||
print(" Open the Studio UI to sign in and change it.")
|
||||
print(f" password: {bootstrap_pw}\n")
|
||||
print("=" * 60 + "\n")
|
||||
else:
|
||||
app.state.bootstrap_password = storage.get_bootstrap_password()
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ interface ModelSelectorProps {
|
|||
activeGgufVariant?: string | null;
|
||||
onValueChange?: (value: string, meta: ModelSelectorChangeMeta) => void;
|
||||
onEject?: () => void;
|
||||
onFoldersChange?: () => void;
|
||||
variant?: "outline" | "ghost" | "muted";
|
||||
size?: "sm" | "default" | "lg";
|
||||
className?: string;
|
||||
|
|
@ -101,7 +100,6 @@ function ModelSelectorContent({
|
|||
value,
|
||||
onSelect,
|
||||
onEject,
|
||||
onFoldersChange,
|
||||
className,
|
||||
dataTour,
|
||||
}: {
|
||||
|
|
@ -110,7 +108,6 @@ function ModelSelectorContent({
|
|||
value?: string;
|
||||
onSelect: (id: string, meta: ModelSelectorChangeMeta) => void;
|
||||
onEject?: () => void;
|
||||
onFoldersChange?: () => void;
|
||||
className?: string;
|
||||
dataTour?: string;
|
||||
}) {
|
||||
|
|
@ -127,7 +124,7 @@ function ModelSelectorContent({
|
|||
)}
|
||||
>
|
||||
{chatOnly ? (
|
||||
<HubModelPicker models={models} value={value} onSelect={onSelect} onFoldersChange={onFoldersChange} />
|
||||
<HubModelPicker models={models} value={value} onSelect={onSelect} />
|
||||
) : (
|
||||
<Tabs defaultValue="hub" className="w-full">
|
||||
<TabsList className="mb-2 w-full">
|
||||
|
|
@ -136,7 +133,7 @@ function ModelSelectorContent({
|
|||
</TabsList>
|
||||
|
||||
<TabsContent value="hub" className="m-0">
|
||||
<HubModelPicker models={models} value={value} onSelect={onSelect} onFoldersChange={onFoldersChange} />
|
||||
<HubModelPicker models={models} value={value} onSelect={onSelect} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="lora" className="m-0">
|
||||
|
|
@ -174,7 +171,6 @@ export function ModelSelector({
|
|||
activeGgufVariant,
|
||||
onValueChange,
|
||||
onEject,
|
||||
onFoldersChange,
|
||||
variant = "outline",
|
||||
size = "default",
|
||||
className,
|
||||
|
|
@ -257,7 +253,6 @@ export function ModelSelector({
|
|||
value={selected}
|
||||
onSelect={handleSelect}
|
||||
onEject={onEject ? handleEject : undefined}
|
||||
onFoldersChange={onFoldersChange}
|
||||
className={contentClassName}
|
||||
dataTour={contentDataTour}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -20,15 +20,11 @@ import {
|
|||
} from "@/components/ui/tooltip";
|
||||
import { usePlatformStore } from "@/config/env";
|
||||
import {
|
||||
type ScanFolderInfo,
|
||||
addScanFolder,
|
||||
deleteCachedModel,
|
||||
listCachedGguf,
|
||||
listCachedModels,
|
||||
listGgufVariants,
|
||||
listLocalModels,
|
||||
listScanFolders,
|
||||
removeScanFolder,
|
||||
} from "@/features/chat/api/chat-api";
|
||||
import type {
|
||||
CachedGgufRepo,
|
||||
|
|
@ -46,7 +42,7 @@ import {
|
|||
import { cn, formatCompact } from "@/lib/utils";
|
||||
import type { VramFitStatus } from "@/lib/vram";
|
||||
import { checkVramFit, estimateLoadingVram } from "@/lib/vram";
|
||||
import { Add01Icon, Cancel01Icon, Folder02Icon, Search01Icon } from "@hugeicons/core-free-icons";
|
||||
import { Search01Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Trash2Icon } from "lucide-react";
|
||||
import {
|
||||
|
|
@ -419,7 +415,6 @@ let _cachedGgufCache: CachedGgufRepo[] = [];
|
|||
let _cachedModelsCache: CachedModelRepo[] = [];
|
||||
let _lmStudioCache: LocalModelInfo[] = [];
|
||||
let _customFolderCache: LocalModelInfo[] = [];
|
||||
let _scanFoldersCache: ScanFolderInfo[] = [];
|
||||
|
||||
/** Sort LM Studio models with unsloth publisher first. */
|
||||
function sortLmStudio(models: LocalModelInfo[]): LocalModelInfo[] {
|
||||
|
|
@ -439,12 +434,10 @@ export function HubModelPicker({
|
|||
models,
|
||||
value,
|
||||
onSelect,
|
||||
onFoldersChange,
|
||||
}: {
|
||||
models: ModelOption[];
|
||||
value?: string;
|
||||
onSelect: (id: string, meta: ModelSelectorChangeMeta) => void;
|
||||
onFoldersChange?: () => void;
|
||||
}) {
|
||||
const gpu = useGpuInfo();
|
||||
const [query, setQuery] = useState("");
|
||||
|
|
@ -476,13 +469,6 @@ export function HubModelPicker({
|
|||
const [customFolderModels, setCustomFolderModels] =
|
||||
useState<LocalModelInfo[]>(_customFolderCache);
|
||||
|
||||
// Custom scan folders management
|
||||
const [scanFolders, setScanFolders] = useState<ScanFolderInfo[]>(_scanFoldersCache);
|
||||
const [folderInput, setFolderInput] = useState("");
|
||||
const [folderError, setFolderError] = useState<string | null>(null);
|
||||
const [showFolderInput, setShowFolderInput] = useState(false);
|
||||
const [folderLoading, setFolderLoading] = useState(false);
|
||||
|
||||
const refreshLocalModelsList = useCallback(() => {
|
||||
listLocalModels()
|
||||
.then((res) => {
|
||||
|
|
@ -498,57 +484,6 @@ export function HubModelPicker({
|
|||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const refreshScanFolders = useCallback(() => {
|
||||
listScanFolders()
|
||||
.then((v) => {
|
||||
_scanFoldersCache = v;
|
||||
setScanFolders(v);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const handleAddFolder = useCallback(async () => {
|
||||
const trimmed = folderInput.trim();
|
||||
if (!trimmed || folderLoading) return;
|
||||
setFolderError(null);
|
||||
setFolderLoading(true);
|
||||
try {
|
||||
const created = await addScanFolder(trimmed);
|
||||
// Backend returns existing row for duplicates, so deduplicate
|
||||
const next = _scanFoldersCache.some((f) => f.id === created.id || f.path === created.path)
|
||||
? _scanFoldersCache
|
||||
: [..._scanFoldersCache, created];
|
||||
_scanFoldersCache = next;
|
||||
setScanFolders(next);
|
||||
setFolderInput("");
|
||||
setShowFolderInput(false);
|
||||
refreshLocalModelsList();
|
||||
onFoldersChange?.();
|
||||
// Background reconciliation with the server
|
||||
void refreshScanFolders();
|
||||
} catch (e) {
|
||||
setFolderError(e instanceof Error ? e.message : "Failed to add folder");
|
||||
} finally {
|
||||
setFolderLoading(false);
|
||||
}
|
||||
}, [folderInput, folderLoading, refreshScanFolders, refreshLocalModelsList, onFoldersChange]);
|
||||
|
||||
const handleRemoveFolder = useCallback(async (id: number) => {
|
||||
try {
|
||||
await removeScanFolder(id);
|
||||
// Optimistic update so the folder disappears immediately
|
||||
const next = _scanFoldersCache.filter((f) => f.id !== id);
|
||||
_scanFoldersCache = next;
|
||||
setScanFolders(next);
|
||||
refreshScanFolders();
|
||||
refreshLocalModelsList();
|
||||
onFoldersChange?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to remove folder");
|
||||
refreshScanFolders();
|
||||
}
|
||||
}, [refreshScanFolders, refreshLocalModelsList, onFoldersChange]);
|
||||
|
||||
const refreshCachedLists = useCallback(() => {
|
||||
listCachedGguf()
|
||||
.then((v) => {
|
||||
|
|
@ -568,7 +503,6 @@ export function HubModelPicker({
|
|||
useEffect(() => {
|
||||
// Always refresh LM Studio + custom folder models (not gated by alreadyCached)
|
||||
refreshLocalModelsList();
|
||||
refreshScanFolders();
|
||||
|
||||
if (alreadyCached) return;
|
||||
let done = 0;
|
||||
|
|
@ -589,7 +523,7 @@ export function HubModelPicker({
|
|||
})
|
||||
.catch(() => {})
|
||||
.finally(check);
|
||||
}, [alreadyCached, refreshLocalModelsList, refreshScanFolders]);
|
||||
}, [alreadyCached]);
|
||||
|
||||
const handleDeleteConfirm = useCallback(async () => {
|
||||
if (!deleteTarget) return;
|
||||
|
|
@ -944,95 +878,9 @@ export function HubModelPicker({
|
|||
</>
|
||||
) : null}
|
||||
|
||||
{!showHfSection ? (
|
||||
{!showHfSection && customFolderModels.length > 0 ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-2.5 py-1.5">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Custom Folders
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={showFolderInput ? "Cancel adding folder" : "Add scan folder"}
|
||||
onClick={() => {
|
||||
setShowFolderInput((open) => {
|
||||
if (open) { setFolderInput(""); setFolderError(null); }
|
||||
return !open;
|
||||
});
|
||||
}}
|
||||
className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon icon={showFolderInput ? Cancel01Icon : Add01Icon} className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Folder paths */}
|
||||
{scanFolders.map((f) => (
|
||||
<div
|
||||
key={f.id}
|
||||
className="group flex items-center gap-1.5 px-3 py-0.5"
|
||||
>
|
||||
<HugeiconsIcon icon={Folder02Icon} className="size-3 shrink-0 text-muted-foreground/40" />
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground/70"
|
||||
title={f.path}
|
||||
>
|
||||
{f.path}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFolder(f.id)}
|
||||
aria-label={`Remove folder ${f.path}`}
|
||||
className="shrink-0 rounded p-0.5 text-muted-foreground/40 opacity-100 md:opacity-0 md:group-hover:opacity-100 focus-visible:opacity-100 transition-opacity hover:text-destructive"
|
||||
>
|
||||
<HugeiconsIcon icon={Cancel01Icon} className="size-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add folder input */}
|
||||
{showFolderInput && (
|
||||
<div className="px-2.5 pb-1 pt-0.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<HugeiconsIcon icon={Folder02Icon} className="size-3 shrink-0 text-muted-foreground/40" />
|
||||
<input
|
||||
value={folderInput}
|
||||
onChange={(e) => { setFolderInput(e.target.value); setFolderError(null); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") { e.preventDefault(); handleAddFolder(); }
|
||||
if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); setShowFolderInput(false); setFolderInput(""); setFolderError(null); }
|
||||
}}
|
||||
placeholder="/path/to/models"
|
||||
className="h-6 min-w-0 flex-1 rounded border border-border/50 bg-transparent px-1.5 font-mono text-[10px] text-foreground outline-none placeholder:text-muted-foreground/40 focus:border-foreground/20"
|
||||
disabled={folderLoading}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddFolder}
|
||||
disabled={folderLoading || !folderInput.trim()}
|
||||
className="h-6 shrink-0 rounded border border-border/50 px-1.5 text-[10px] text-muted-foreground transition-colors hover:bg-accent disabled:opacity-40"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{folderError && (
|
||||
<p className="px-0.5 pt-0.5 text-[10px] text-destructive">{folderError}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{scanFolders.length === 0 && customFolderModels.length === 0 && !showFolderInput && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFolderInput(true)}
|
||||
className="px-2.5 pb-1.5 text-left text-[10px] text-muted-foreground/60 transition-colors hover:text-muted-foreground"
|
||||
>
|
||||
+ Add a folder to scan for local models
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Models from custom folders */}
|
||||
<ListLabel>Custom Folders</ListLabel>
|
||||
{customFolderModels.map((m) => {
|
||||
const isGguf =
|
||||
isGgufRepo(m.id) ||
|
||||
|
|
|
|||
|
|
@ -162,12 +162,10 @@ const CompareContent = memo(function CompareContent({
|
|||
pairId,
|
||||
models,
|
||||
loraModels,
|
||||
onFoldersChange,
|
||||
}: {
|
||||
pairId: string;
|
||||
models: ModelOption[];
|
||||
loraModels: LoraModelOption[];
|
||||
onFoldersChange?: () => void;
|
||||
}): ReactElement {
|
||||
const isLoraCompare = useIsLoraCompare();
|
||||
|
||||
|
|
@ -178,7 +176,6 @@ const CompareContent = memo(function CompareContent({
|
|||
pairId={pairId}
|
||||
models={models}
|
||||
loraModels={loraModels}
|
||||
onFoldersChange={onFoldersChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
@ -262,12 +259,10 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
|||
pairId,
|
||||
models,
|
||||
loraModels,
|
||||
onFoldersChange,
|
||||
}: {
|
||||
pairId: string;
|
||||
models: ModelOption[];
|
||||
loraModels: LoraModelOption[];
|
||||
onFoldersChange?: () => void;
|
||||
}): ReactElement {
|
||||
const handlesRef = useRef<Record<string, CompareHandle>>({});
|
||||
const [model1ThreadId, setModel1ThreadId] = useState<string>();
|
||||
|
|
@ -332,7 +327,6 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
|||
ggufVariant: meta.ggufVariant,
|
||||
})
|
||||
}
|
||||
onFoldersChange={onFoldersChange}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="max-w-[50%]"
|
||||
|
|
@ -365,7 +359,6 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
|||
ggufVariant: meta.ggufVariant,
|
||||
})
|
||||
}
|
||||
onFoldersChange={onFoldersChange}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="max-w-[50%]"
|
||||
|
|
@ -853,7 +846,6 @@ export function ChatPage(): ReactElement {
|
|||
activeGgufVariant={activeGgufVariant}
|
||||
onValueChange={handleCheckpointChange}
|
||||
onEject={handleEject}
|
||||
onFoldersChange={refreshLocalModels}
|
||||
variant="ghost"
|
||||
open={modelSelectorOpen}
|
||||
onOpenChange={handleModelSelectorOpenChange}
|
||||
|
|
@ -919,7 +911,6 @@ export function ChatPage(): ReactElement {
|
|||
pairId={view.pairId}
|
||||
models={models}
|
||||
loraModels={loraModels}
|
||||
onFoldersChange={refreshLocalModels}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -943,6 +934,7 @@ export function ChatPage(): ReactElement {
|
|||
});
|
||||
}
|
||||
}}
|
||||
onFoldersChange={refreshLocalModels}
|
||||
/>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
CodeIcon,
|
||||
Delete02Icon,
|
||||
FloppyDiskIcon,
|
||||
FolderSearchIcon,
|
||||
PencilEdit01Icon,
|
||||
Settings02Icon,
|
||||
SlidersHorizontalIcon,
|
||||
|
|
@ -43,7 +44,13 @@ import {
|
|||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
type ScanFolderInfo,
|
||||
addScanFolder,
|
||||
listScanFolders,
|
||||
removeScanFolder,
|
||||
} from "./api/chat-api";
|
||||
import { useChatRuntimeStore } from "./stores/chat-runtime-store";
|
||||
import {
|
||||
DEFAULT_INFERENCE_PARAMS,
|
||||
|
|
@ -259,6 +266,108 @@ function CollapsibleSection({
|
|||
);
|
||||
}
|
||||
|
||||
function ModelFoldersSection({
|
||||
onFoldersChange,
|
||||
}: { onFoldersChange?: () => void }) {
|
||||
const [folders, setFolders] = useState<ScanFolderInfo[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
listScanFolders()
|
||||
.then(setFolders)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return;
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
await addScanFolder(trimmed);
|
||||
setInput("");
|
||||
refresh();
|
||||
onFoldersChange?.();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to add folder");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = async (id: number) => {
|
||||
try {
|
||||
await removeScanFolder(id);
|
||||
onFoldersChange?.();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to remove folder");
|
||||
} finally {
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CollapsibleSection icon={FolderSearchIcon} label="Model Folders">
|
||||
<div className="flex flex-col gap-2 py-1">
|
||||
{folders.length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
{folders.map((f) => (
|
||||
<div
|
||||
key={f.id}
|
||||
className="group flex items-center gap-1.5 rounded-md px-1.5 py-1 text-xs transition-colors hover:bg-accent"
|
||||
>
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate text-muted-foreground"
|
||||
title={f.path}
|
||||
>
|
||||
{f.path}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(f.id)}
|
||||
className="shrink-0 rounded p-0.5 text-muted-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 hover:text-destructive"
|
||||
>
|
||||
<HugeiconsIcon icon={Delete02Icon} className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-1.5">
|
||||
<Input
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleAdd();
|
||||
}}
|
||||
placeholder="/path/to/models"
|
||||
className="h-7 flex-1 text-xs font-mono"
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAdd}
|
||||
disabled={loading || !input.trim()}
|
||||
className="h-7 rounded-md border px-2 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-[11px] text-destructive">{error}</p>}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChatSettingsPanelProps {
|
||||
open: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
|
|
@ -267,6 +376,7 @@ interface ChatSettingsPanelProps {
|
|||
autoTitle: boolean;
|
||||
onAutoTitleChange: (enabled: boolean) => void;
|
||||
onReloadModel?: () => void;
|
||||
onFoldersChange?: () => void;
|
||||
}
|
||||
|
||||
export function ChatSettingsPanel({
|
||||
|
|
@ -277,6 +387,7 @@ export function ChatSettingsPanel({
|
|||
autoTitle,
|
||||
onAutoTitleChange,
|
||||
onReloadModel,
|
||||
onFoldersChange,
|
||||
}: ChatSettingsPanelProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
|
||||
|
|
@ -724,6 +835,8 @@ export function ChatSettingsPanel({
|
|||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<ModelFoldersSection onFoldersChange={onFoldersChange} />
|
||||
|
||||
<ChatTemplateSection onReloadModel={onReloadModel} />
|
||||
</div>
|
||||
<Dialog
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -490,7 +490,8 @@ if (-not $HasNvidiaSmi) {
|
|||
if (-not $HasNvidiaSmi) {
|
||||
Write-Host ""
|
||||
step "gpu" "none (chat-only / GGUF)" "Yellow"
|
||||
substep "Training and GPU inference require an NVIDIA GPU with drivers installed." "Yellow"
|
||||
Write-Host " Training and GPU inference require an NVIDIA GPU with drivers installed." -ForegroundColor Yellow
|
||||
Write-Host " https://www.nvidia.com/Download/index.aspx" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
} else {
|
||||
step "gpu" "NVIDIA GPU detected"
|
||||
|
|
@ -1596,29 +1597,15 @@ $resolveExit = $LASTEXITCODE
|
|||
$ResolvedLlamaTag = if ($resolveOutput) { ($resolveOutput | Select-Object -Last 1).ToString().Trim() } else { "" }
|
||||
if ($resolveExit -ne 0 -or [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) {
|
||||
Write-Host ""
|
||||
substep "Failed to resolve an installable prebuilt llama.cpp tag via $HelperReleaseRepo" "Yellow"
|
||||
substep "Failed to resolve a published llama.cpp release via $HelperReleaseRepo" "Yellow"
|
||||
Write-LlamaFailureLog -Output ($resolveOutput | Out-String)
|
||||
# Resolve the llama.cpp tag for source-build fallback. Pass --published-repo
|
||||
# so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream
|
||||
# bleeding-edge tag (e.g. b8514) from ggml-org/llama.cpp.
|
||||
# so the resolver prefers the latest usable Unsloth-published upstream tag
|
||||
# before falling back to the bleeding-edge ggml-org/llama.cpp tag.
|
||||
$fallbackOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" --resolve-llama-tag $RequestedLlamaTag --published-repo $HelperReleaseRepo 2>$null
|
||||
$fallbackExit = $LASTEXITCODE
|
||||
$ResolvedLlamaTag = if ($fallbackExit -eq 0 -and $fallbackOutput) {
|
||||
($fallbackOutput | Select-Object -Last 1).ToString().Trim()
|
||||
} elseif ($RequestedLlamaTag -eq "latest") {
|
||||
# Try Unsloth release repo first, then fall back to ggml-org upstream
|
||||
$resolvedLatest = $null
|
||||
try {
|
||||
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/$HelperReleaseRepo/releases/latest" -ErrorAction Stop
|
||||
$resolvedLatest = $latestRelease.tag_name
|
||||
} catch {}
|
||||
if (-not $resolvedLatest) {
|
||||
try {
|
||||
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest" -ErrorAction Stop
|
||||
$resolvedLatest = $latestRelease.tag_name
|
||||
} catch {}
|
||||
}
|
||||
if ($resolvedLatest) { $resolvedLatest } else { $RequestedLlamaTag }
|
||||
} else {
|
||||
$RequestedLlamaTag
|
||||
}
|
||||
|
|
@ -1667,7 +1654,19 @@ if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") {
|
|||
$ErrorActionPreference = $prevEAPPrebuilt
|
||||
|
||||
if ($prebuiltExit -eq 0) {
|
||||
step "llama.cpp" "prebuilt installed and validated"
|
||||
if ($prebuiltOutput -match "already matches selected release") {
|
||||
step "llama.cpp" "prebuilt up to date and validated"
|
||||
} else {
|
||||
step "llama.cpp" "prebuilt installed and validated"
|
||||
}
|
||||
} elseif ($prebuiltExit -eq 3) {
|
||||
step "llama.cpp" "install blocked by active llama.cpp process" "Yellow"
|
||||
Write-LlamaFailureLog -Output $prebuiltOutput
|
||||
if (Test-Path $LlamaCppDir) {
|
||||
substep "Existing install was restored" "Yellow"
|
||||
}
|
||||
substep "Close Studio or other llama.cpp users and retry" "Yellow"
|
||||
exit 3
|
||||
} else {
|
||||
step "llama.cpp" "prebuilt install failed (continuing)" "Yellow"
|
||||
Write-LlamaFailureLog -Output $prebuiltOutput
|
||||
|
|
|
|||
|
|
@ -108,6 +108,10 @@ echo ""
|
|||
printf " ${C_TITLE}%s${C_RST}\n" "🦥 Unsloth Studio Setup"
|
||||
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
|
||||
verbose_substep "verbose diagnostics enabled"
|
||||
_LLAMA_ONLY="${UNSLOTH_STUDIO_LLAMA_ONLY:-0}"
|
||||
if [ "$_LLAMA_ONLY" = "1" ]; then
|
||||
substep "llama.cpp only mode"
|
||||
fi
|
||||
# ── Clean up stale caches ──
|
||||
rm -rf "$REPO_ROOT/unsloth_compiled_cache"
|
||||
rm -rf "$SCRIPT_DIR/backend/unsloth_compiled_cache"
|
||||
|
|
@ -120,6 +124,7 @@ if [[ "$keynames" == *$'\nCOLAB_'* ]]; then
|
|||
IS_COLAB=true
|
||||
fi
|
||||
|
||||
if [ "$_LLAMA_ONLY" != "1" ]; then
|
||||
# ── Frontend ──
|
||||
_NEED_FRONTEND_BUILD=true
|
||||
if [ -d "$SCRIPT_DIR/frontend/dist" ]; then
|
||||
|
|
@ -453,6 +458,7 @@ else
|
|||
step "python" "dependencies up to date"
|
||||
verbose_substep "python deps check: installed=$_PKG_NAME@${INSTALLED_VER:-unknown} latest=${LATEST_VER:-unknown}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 7. Prefer prebuilt llama.cpp bundles before any source build path ──
|
||||
UNSLOTH_HOME="$HOME/.unsloth"
|
||||
|
|
@ -477,26 +483,17 @@ else
|
|||
_RESOLVED_LLAMA_TAG=""
|
||||
fi
|
||||
if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
|
||||
step "llama.cpp" "failed to resolve prebuilt tag via $_HELPER_RELEASE_REPO" "$C_WARN"
|
||||
step "llama.cpp" "failed to resolve a published llama.cpp release via $_HELPER_RELEASE_REPO" "$C_WARN"
|
||||
print_llama_error_log "$_RESOLVE_LLAMA_LOG"
|
||||
set +e
|
||||
# Resolve the llama.cpp tag for source-build fallback. Pass --published-repo
|
||||
# so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream
|
||||
# bleeding-edge tag (e.g. b8514) from ggml-org/llama.cpp.
|
||||
# so the resolver prefers the latest usable Unsloth-published upstream tag
|
||||
# before falling back to the bleeding-edge ggml-org/llama.cpp tag.
|
||||
_RESOLVED_LLAMA_TAG="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" --resolve-llama-tag "$_REQUESTED_LLAMA_TAG" --published-repo "$_HELPER_RELEASE_REPO" 2>/dev/null)"
|
||||
_RESOLVE_UPSTREAM_STATUS=$?
|
||||
set -e
|
||||
if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then
|
||||
if [ "$_REQUESTED_LLAMA_TAG" = "latest" ]; then
|
||||
# Try Unsloth release repo first, then fall back to ggml-org upstream
|
||||
_RESOLVED_LLAMA_TAG="$(curl -fsSL "https://api.github.com/repos/${_HELPER_RELEASE_REPO}/releases/latest" 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG=""
|
||||
if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
|
||||
_RESOLVED_LLAMA_TAG="$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG=""
|
||||
fi
|
||||
fi
|
||||
if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
|
||||
_RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG"
|
||||
fi
|
||||
_RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG"
|
||||
fi
|
||||
_NEED_LLAMA_SOURCE_BUILD=true
|
||||
_SKIP_PREBUILT_INSTALL=true
|
||||
|
|
@ -538,9 +535,22 @@ else
|
|||
set -e
|
||||
|
||||
if [ "$_PREBUILT_STATUS" -eq 0 ]; then
|
||||
step "llama.cpp" "prebuilt installed and validated"
|
||||
if grep -Fq "already matches selected release" "$_PREBUILT_LOG"; then
|
||||
step "llama.cpp" "prebuilt up to date and validated"
|
||||
else
|
||||
step "llama.cpp" "prebuilt installed and validated"
|
||||
fi
|
||||
verbose_substep "llama.cpp install dir: $LLAMA_CPP_DIR"
|
||||
rm -f "$_PREBUILT_LOG"
|
||||
elif [ "$_PREBUILT_STATUS" -eq 3 ]; then
|
||||
step "llama.cpp" "install blocked by active llama.cpp process" "$C_WARN"
|
||||
print_llama_error_log "$_PREBUILT_LOG"
|
||||
rm -f "$_PREBUILT_LOG"
|
||||
if [ -d "$LLAMA_CPP_DIR" ]; then
|
||||
substep "existing install was restored"
|
||||
fi
|
||||
substep "close Studio or other llama.cpp users and retry"
|
||||
exit 3
|
||||
else
|
||||
step "llama.cpp" "prebuilt install failed (continuing)" "$C_WARN"
|
||||
print_llama_error_log "$_PREBUILT_LOG"
|
||||
|
|
@ -794,7 +804,16 @@ else
|
|||
fi # end _SKIP_GGUF_BUILD check
|
||||
|
||||
# ── Footer ──
|
||||
if [ "$IS_COLAB" = true ]; then
|
||||
if [ "$_LLAMA_ONLY" = "1" ]; then
|
||||
echo ""
|
||||
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
|
||||
if [ "$_LLAMA_CPP_DEGRADED" = true ]; then
|
||||
printf " ${C_WARN}%s${C_RST}\n" "llama.cpp update finished (limited: llama.cpp unavailable)"
|
||||
else
|
||||
printf " ${C_TITLE}%s${C_RST}\n" "llama.cpp update finished"
|
||||
fi
|
||||
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
|
||||
elif [ "$IS_COLAB" = true ]; then
|
||||
echo ""
|
||||
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
|
||||
if [ "$_LLAMA_CPP_DEGRADED" = true ]; then
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ def parse_args() -> argparse.Namespace:
|
|||
parser.add_argument(
|
||||
"--llama-tag",
|
||||
default = "latest",
|
||||
help = "llama.cpp tag to resolve. Defaults to the approved prebuilt tag for this host.",
|
||||
help = "llama.cpp tag to resolve. Defaults to the latest usable published Unsloth release.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--published-repo",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,7 @@ Tests cover:
|
|||
- Bug 2: Source-build fallback ignores pinned tag (both .sh and .ps1)
|
||||
- Bug 3: Unix fallback deletes install before checking prerequisites
|
||||
- Bug 4: Linux LD_LIBRARY_PATH missing build/bin
|
||||
- "latest" tag resolution fallback chain (Unsloth -> ggml-org -> raw)
|
||||
- "latest" tag resolution fallback chain (helper -> raw)
|
||||
- Cross-platform binary_env (Linux, macOS, Windows)
|
||||
- Edge cases: malformed JSON, empty responses, env overrides
|
||||
|
||||
|
|
@ -40,6 +40,10 @@ SPEC.loader.exec_module(MOD)
|
|||
binary_env = MOD.binary_env
|
||||
HostInfo = MOD.HostInfo
|
||||
resolve_requested_llama_tag = MOD.resolve_requested_llama_tag
|
||||
PublishedReleaseBundle = MOD.PublishedReleaseBundle
|
||||
ApprovedArtifactHash = MOD.ApprovedArtifactHash
|
||||
ApprovedReleaseChecksums = MOD.ApprovedReleaseChecksums
|
||||
source_archive_logical_name = MOD.source_archive_logical_name
|
||||
|
||||
SETUP_SH = PACKAGE_ROOT / "studio" / "setup.sh"
|
||||
SETUP_PS1 = PACKAGE_ROOT / "studio" / "setup.ps1"
|
||||
|
|
@ -240,6 +244,57 @@ class TestResolveRequestedLlamaTag:
|
|||
monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b5555")
|
||||
assert resolve_requested_llama_tag("") == "b5555"
|
||||
|
||||
def test_latest_with_published_repo_uses_latest_valid_published_release(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
invalid = PublishedReleaseBundle(
|
||||
repo = "unslothai/llama.cpp",
|
||||
release_tag = "v2.0",
|
||||
upstream_tag = "b9000",
|
||||
assets = {},
|
||||
manifest_asset_name = "llama-prebuilt-manifest.json",
|
||||
artifacts = [],
|
||||
selection_log = [],
|
||||
)
|
||||
valid = PublishedReleaseBundle(
|
||||
repo = "unslothai/llama.cpp",
|
||||
release_tag = "v1.0",
|
||||
upstream_tag = "b8999",
|
||||
assets = {},
|
||||
manifest_asset_name = "llama-prebuilt-manifest.json",
|
||||
artifacts = [],
|
||||
selection_log = [],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
MOD,
|
||||
"iter_published_release_bundles",
|
||||
lambda repo, published_release_tag = "": iter([invalid, valid]),
|
||||
)
|
||||
|
||||
def fake_load(repo, release_tag):
|
||||
if release_tag == "v2.0":
|
||||
raise MOD.PrebuiltFallback("checksum asset missing")
|
||||
return ApprovedReleaseChecksums(
|
||||
repo = repo,
|
||||
release_tag = release_tag,
|
||||
upstream_tag = "b8999",
|
||||
source_commit = None,
|
||||
artifacts = {
|
||||
source_archive_logical_name("b8999"): ApprovedArtifactHash(
|
||||
asset_name = source_archive_logical_name("b8999"),
|
||||
sha256 = "a" * 64,
|
||||
repo = "ggml-org/llama.cpp",
|
||||
kind = "upstream-source",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(MOD, "load_approved_release_checksums", fake_load)
|
||||
monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b7777")
|
||||
|
||||
assert resolve_requested_llama_tag("latest", "unslothai/llama.cpp") == "b8999"
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# TEST GROUP C: setup.sh logic (bash subprocess tests)
|
||||
|
|
@ -429,140 +484,61 @@ class TestSetupShLogic:
|
|||
# TEST GROUP D: "latest" tag resolution (bash subprocess)
|
||||
# =========================================================================
|
||||
class TestLatestTagResolution:
|
||||
"""Test the fallback chain: Unsloth API -> ggml-org API -> raw."""
|
||||
"""Test the fallback chain: helper resolver -> raw."""
|
||||
|
||||
RESOLVE_TEMPLATE = textwrap.dedent("""\
|
||||
export PATH="{mock_bin}:$PATH"
|
||||
_REQUESTED_LLAMA_TAG="{requested_tag}"
|
||||
_RESOLVED_LLAMA_TAG=""
|
||||
_RESOLVE_UPSTREAM_STATUS=1
|
||||
_HELPER_RELEASE_REPO="unslothai/llama.cpp"
|
||||
if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then
|
||||
if [ "$_REQUESTED_LLAMA_TAG" = "latest" ]; then
|
||||
_RESOLVED_LLAMA_TAG="$(curl -fsSL "https://api.github.com/repos/${{_HELPER_RELEASE_REPO}}/releases/latest" 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG=""
|
||||
if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
|
||||
_RESOLVED_LLAMA_TAG="$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG=""
|
||||
fi
|
||||
fi
|
||||
if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
|
||||
_RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG"
|
||||
fi
|
||||
_RESOLVE_UPSTREAM_STATUS={resolve_status}
|
||||
if [ "$_RESOLVE_UPSTREAM_STATUS" -eq 0 ] && [ -n "{resolved_tag}" ]; then
|
||||
_RESOLVED_LLAMA_TAG="{resolved_tag}"
|
||||
else
|
||||
_RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG"
|
||||
fi
|
||||
echo "$_RESOLVED_LLAMA_TAG"
|
||||
""")
|
||||
|
||||
@staticmethod
|
||||
def _make_curl_mock(
|
||||
mock_bin: Path, unsloth_response: str | None, ggml_response: str | None
|
||||
):
|
||||
"""Create a curl mock that returns different responses per repo."""
|
||||
lines = ["#!/bin/bash"]
|
||||
if unsloth_response is not None:
|
||||
lines.append(
|
||||
f'if echo "$*" | grep -q "unslothai/llama.cpp"; then echo \'{unsloth_response}\'; exit 0; fi'
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
'if echo "$*" | grep -q "unslothai/llama.cpp"; then exit 1; fi'
|
||||
)
|
||||
if ggml_response is not None:
|
||||
lines.append(
|
||||
f'if echo "$*" | grep -q "ggml-org/llama.cpp"; then echo \'{ggml_response}\'; exit 0; fi'
|
||||
)
|
||||
else:
|
||||
lines.append('if echo "$*" | grep -q "ggml-org/llama.cpp"; then exit 1; fi')
|
||||
lines.append("exit 1")
|
||||
curl_path = mock_bin / "curl"
|
||||
curl_path.write_text("\n".join(lines) + "\n")
|
||||
curl_path.chmod(0o755)
|
||||
|
||||
def _run_resolve(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
requested_tag: str,
|
||||
unsloth_resp: str | None,
|
||||
ggml_resp: str | None,
|
||||
resolved_tag: str,
|
||||
resolve_status: int,
|
||||
) -> str:
|
||||
mock_bin = tmp_path / "mock_bin"
|
||||
mock_bin.mkdir(exist_ok = True)
|
||||
self._make_curl_mock(mock_bin, unsloth_resp, ggml_resp)
|
||||
script = self.RESOLVE_TEMPLATE.format(
|
||||
mock_bin = mock_bin, requested_tag = requested_tag
|
||||
requested_tag = requested_tag,
|
||||
resolved_tag = resolved_tag,
|
||||
resolve_status = resolve_status,
|
||||
)
|
||||
return run_bash(script)
|
||||
|
||||
def test_unsloth_succeeds(self, tmp_path: Path):
|
||||
def test_helper_resolution_succeeds(self, tmp_path: Path):
|
||||
output = self._run_resolve(
|
||||
tmp_path,
|
||||
"latest",
|
||||
unsloth_resp = '{"tag_name":"b8508"}',
|
||||
ggml_resp = '{"tag_name":"b9000"}',
|
||||
resolved_tag = "b8508",
|
||||
resolve_status = 0,
|
||||
)
|
||||
assert output == "b8508"
|
||||
|
||||
def test_unsloth_fails_ggml_succeeds(self, tmp_path: Path):
|
||||
def test_helper_resolution_falls_back_to_raw_requested_tag(self, tmp_path: Path):
|
||||
output = self._run_resolve(
|
||||
tmp_path,
|
||||
"latest",
|
||||
unsloth_resp = None,
|
||||
ggml_resp = '{"tag_name":"b9000"}',
|
||||
)
|
||||
assert output == "b9000"
|
||||
|
||||
def test_both_fail_raw_fallback(self, tmp_path: Path):
|
||||
output = self._run_resolve(
|
||||
tmp_path,
|
||||
"latest",
|
||||
unsloth_resp = None,
|
||||
ggml_resp = None,
|
||||
resolved_tag = "",
|
||||
resolve_status = 1,
|
||||
)
|
||||
assert output == "latest"
|
||||
|
||||
def test_concrete_tag_passes_through(self, tmp_path: Path):
|
||||
def test_concrete_tag_passes_through_when_helper_fails(self, tmp_path: Path):
|
||||
output = self._run_resolve(
|
||||
tmp_path,
|
||||
"b7777",
|
||||
unsloth_resp = '{"tag_name":"b8508"}',
|
||||
ggml_resp = '{"tag_name":"b9000"}',
|
||||
resolved_tag = "",
|
||||
resolve_status = 1,
|
||||
)
|
||||
assert output == "b7777"
|
||||
|
||||
def test_unsloth_malformed_json_falls_through(self, tmp_path: Path):
|
||||
output = self._run_resolve(
|
||||
tmp_path,
|
||||
"latest",
|
||||
unsloth_resp = '{"bad_key":"no_tag"}',
|
||||
ggml_resp = '{"tag_name":"b9001"}',
|
||||
)
|
||||
assert output == "b9001"
|
||||
|
||||
def test_both_malformed_json_raw_fallback(self, tmp_path: Path):
|
||||
output = self._run_resolve(
|
||||
tmp_path,
|
||||
"latest",
|
||||
unsloth_resp = '{"bad":"data"}',
|
||||
ggml_resp = '{"also":"bad"}',
|
||||
)
|
||||
assert output == "latest"
|
||||
|
||||
def test_unsloth_empty_body_falls_through(self, tmp_path: Path):
|
||||
output = self._run_resolve(
|
||||
tmp_path,
|
||||
"latest",
|
||||
unsloth_resp = "",
|
||||
ggml_resp = '{"tag_name":"b7000"}',
|
||||
)
|
||||
assert output == "b7000"
|
||||
|
||||
def test_unsloth_empty_tag_name_falls_through(self, tmp_path: Path):
|
||||
output = self._run_resolve(
|
||||
tmp_path,
|
||||
"latest",
|
||||
unsloth_resp = '{"tag_name":""}',
|
||||
ggml_resp = '{"tag_name":"b6000"}',
|
||||
)
|
||||
assert output == "b6000"
|
||||
|
||||
def test_env_override_unsloth_llama_tag(self):
|
||||
output = run_bash(
|
||||
'echo "${UNSLOTH_LLAMA_TAG:-latest}"',
|
||||
|
|
@ -593,10 +569,10 @@ class TestSourceCodePatterns:
|
|||
def test_setup_sh_no_rm_before_prereq_check(self):
|
||||
"""rm -rf must appear AFTER cmake/git checks, not before."""
|
||||
content = SETUP_SH.read_text()
|
||||
# Find the source-build block
|
||||
idx_else = content.find("# Check prerequisites")
|
||||
assert idx_else != -1
|
||||
block = content[idx_else:]
|
||||
# Anchor on the source-build cmake check block.
|
||||
idx_block = content.find("command -v cmake")
|
||||
assert idx_block != -1
|
||||
block = content[idx_block:]
|
||||
# rm -rf should appear after the cmake/git checks
|
||||
idx_cmake = block.find("command -v cmake")
|
||||
idx_git = block.find("command -v git")
|
||||
|
|
@ -619,14 +595,12 @@ class TestSourceCodePatterns:
|
|||
'_RESOLVED_LLAMA_TAG" != "latest"' in content
|
||||
), "Should guard against literal 'latest' tag"
|
||||
|
||||
def test_setup_sh_latest_resolution_queries_unsloth_first(self):
|
||||
"""The Unsloth repo should be queried before ggml-org."""
|
||||
def test_setup_sh_latest_resolution_uses_helper_only(self):
|
||||
"""Shell fallback should rely on helper output, not raw GitHub API tag_name."""
|
||||
content = SETUP_SH.read_text()
|
||||
idx_unsloth = content.find("_HELPER_RELEASE_REPO}/releases/latest")
|
||||
idx_ggml = content.find("ggml-org/llama.cpp/releases/latest")
|
||||
assert idx_unsloth != -1, "Unsloth API query not found"
|
||||
assert idx_ggml != -1, "ggml-org API query not found"
|
||||
assert idx_unsloth < idx_ggml, "Unsloth should be queried before ggml-org"
|
||||
assert "--resolve-llama-tag" in content
|
||||
assert "_HELPER_RELEASE_REPO}/releases/latest" not in content
|
||||
assert "ggml-org/llama.cpp/releases/latest" not in content
|
||||
|
||||
def test_setup_ps1_uses_checkout_b(self):
|
||||
"""PS1 should use checkout -B, not checkout --force FETCH_HEAD."""
|
||||
|
|
@ -658,14 +632,12 @@ class TestSourceCodePatterns:
|
|||
f"Found 'git pull' in llama.cpp build section at line {i+1}"
|
||||
)
|
||||
|
||||
def test_setup_ps1_latest_resolution_queries_unsloth_first(self):
|
||||
"""PS1 should query Unsloth repo before ggml-org."""
|
||||
def test_setup_ps1_latest_resolution_uses_helper_only(self):
|
||||
"""PS1 fallback should rely on helper output, not raw GitHub API tag_name."""
|
||||
content = SETUP_PS1.read_text()
|
||||
idx_unsloth = content.find("$HelperReleaseRepo/releases/latest")
|
||||
idx_ggml = content.find("ggml-org/llama.cpp/releases/latest")
|
||||
assert idx_unsloth != -1, "Unsloth API query not found in PS1"
|
||||
assert idx_ggml != -1, "ggml-org API query not found in PS1"
|
||||
assert idx_unsloth < idx_ggml, "Unsloth should be queried before ggml-org"
|
||||
assert "--resolve-llama-tag" in content
|
||||
assert "$HelperReleaseRepo/releases/latest" not in content
|
||||
assert "ggml-org/llama.cpp/releases/latest" not in content
|
||||
|
||||
def test_binary_env_linux_has_binary_parent(self):
|
||||
"""The Linux branch of binary_env should include binary_path.parent."""
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@ apply_approved_hashes = INSTALL_LLAMA_PREBUILT.apply_approved_hashes
|
|||
linux_cuda_choice_from_release = INSTALL_LLAMA_PREBUILT.linux_cuda_choice_from_release
|
||||
windows_cuda_attempts = INSTALL_LLAMA_PREBUILT.windows_cuda_attempts
|
||||
resolve_upstream_asset_choice = INSTALL_LLAMA_PREBUILT.resolve_upstream_asset_choice
|
||||
resolve_requested_install_tag = INSTALL_LLAMA_PREBUILT.resolve_requested_install_tag
|
||||
resolve_install_attempts = INSTALL_LLAMA_PREBUILT.resolve_install_attempts
|
||||
resolve_install_release_plans = INSTALL_LLAMA_PREBUILT.resolve_install_release_plans
|
||||
resolve_published_release = INSTALL_LLAMA_PREBUILT.resolve_published_release
|
||||
source_archive_logical_name = INSTALL_LLAMA_PREBUILT.source_archive_logical_name
|
||||
env_int = INSTALL_LLAMA_PREBUILT.env_int
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -131,6 +137,37 @@ def make_checksums(asset_names):
|
|||
)
|
||||
|
||||
|
||||
def make_checksums_with_source(
|
||||
asset_names,
|
||||
*,
|
||||
release_tag = "v1.0",
|
||||
upstream_tag = "b8508",
|
||||
):
|
||||
return ApprovedReleaseChecksums(
|
||||
repo = "unslothai/llama.cpp",
|
||||
release_tag = release_tag,
|
||||
upstream_tag = upstream_tag,
|
||||
source_commit = None,
|
||||
artifacts = {
|
||||
**{
|
||||
name: ApprovedArtifactHash(
|
||||
asset_name = name,
|
||||
sha256 = "a" * 64,
|
||||
repo = "unslothai/llama.cpp",
|
||||
kind = "prebuilt",
|
||||
)
|
||||
for name in asset_names
|
||||
},
|
||||
source_archive_logical_name(upstream_tag): ApprovedArtifactHash(
|
||||
asset_name = source_archive_logical_name(upstream_tag),
|
||||
sha256 = "b" * 64,
|
||||
repo = "ggml-org/llama.cpp",
|
||||
kind = "upstream-source",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def mock_linux_runtime(monkeypatch, lines):
|
||||
dirs = {line: ["/usr/lib/stub"] for line in lines}
|
||||
monkeypatch.setattr(
|
||||
|
|
@ -408,7 +445,100 @@ class TestApplyApprovedHashes:
|
|||
|
||||
|
||||
# ===========================================================================
|
||||
# J. linux_cuda_choice_from_release -- core selection
|
||||
# J. published release resolution
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestPublishedReleaseResolution:
|
||||
def test_latest_skips_invalid_release_and_uses_next_valid(self, monkeypatch):
|
||||
invalid = make_release([], release_tag = "v2.0", upstream_tag = "b9000")
|
||||
valid = make_release([], release_tag = "v1.0", upstream_tag = "b8999")
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_published_release_bundles",
|
||||
lambda repo, published_release_tag = "": iter([invalid, valid]),
|
||||
)
|
||||
|
||||
def fake_load(repo, release_tag):
|
||||
if release_tag == "v2.0":
|
||||
raise PrebuiltFallback("checksum asset missing")
|
||||
return make_checksums_with_source(
|
||||
[], release_tag = "v1.0", upstream_tag = "b8999"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"load_approved_release_checksums",
|
||||
fake_load,
|
||||
)
|
||||
|
||||
resolved = resolve_published_release("latest", "unslothai/llama.cpp")
|
||||
assert resolved.bundle.release_tag == "v1.0"
|
||||
assert resolved.bundle.upstream_tag == "b8999"
|
||||
assert resolved.checksums.release_tag == "v1.0"
|
||||
|
||||
def test_concrete_tag_matches_manifest_upstream_tag(self, monkeypatch):
|
||||
release = make_release([], release_tag = "release-b8508", upstream_tag = "b8508")
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_published_release_bundles",
|
||||
lambda repo, published_release_tag = "": iter([release]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"load_approved_release_checksums",
|
||||
lambda repo, release_tag: make_checksums_with_source(
|
||||
[],
|
||||
release_tag = release_tag,
|
||||
upstream_tag = "b8508",
|
||||
),
|
||||
)
|
||||
|
||||
assert (
|
||||
resolve_requested_install_tag("b8508", "", "unslothai/llama.cpp") == "b8508"
|
||||
)
|
||||
|
||||
def test_concrete_tag_without_matching_release_raises(self, monkeypatch):
|
||||
release = make_release([], release_tag = "release-b9000", upstream_tag = "b9000")
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_published_release_bundles",
|
||||
lambda repo, published_release_tag = "": iter([release]),
|
||||
)
|
||||
|
||||
with pytest.raises(PrebuiltFallback, match = "matched upstream tag b8508"):
|
||||
resolve_requested_install_tag("b8508", "", "unslothai/llama.cpp")
|
||||
|
||||
def test_pinned_release_must_match_requested_upstream_tag(self, monkeypatch):
|
||||
bundle = make_release(
|
||||
[], release_tag = "llama-prebuilt-latest", upstream_tag = "b9000"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"pinned_published_release_bundle",
|
||||
lambda repo, release_tag: bundle,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"load_approved_release_checksums",
|
||||
lambda repo, release_tag: make_checksums_with_source(
|
||||
[],
|
||||
release_tag = release_tag,
|
||||
upstream_tag = "b9000",
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(PrebuiltFallback, match = "but requested b8508"):
|
||||
resolve_requested_install_tag(
|
||||
"b8508",
|
||||
"llama-prebuilt-latest",
|
||||
"unslothai/llama.cpp",
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# K. linux_cuda_choice_from_release -- core selection
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
|
|
@ -676,7 +806,554 @@ class TestLinuxCudaChoiceFromRelease:
|
|||
|
||||
|
||||
# ===========================================================================
|
||||
# K. windows_cuda_attempts
|
||||
# L. resolve_install_attempts
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestResolveInstallAttempts:
|
||||
def test_windows_cuda_prefers_published_asset_from_selected_release(
|
||||
self, monkeypatch
|
||||
):
|
||||
host = make_host(system = "Windows", machine = "AMD64")
|
||||
host.driver_cuda_version = (12, 4)
|
||||
mock_windows_runtime(monkeypatch, ["cuda12"])
|
||||
asset_name = "llama-b9000-bin-win-cuda-12.4-x64.zip"
|
||||
release = make_release(
|
||||
[
|
||||
make_artifact(
|
||||
asset_name,
|
||||
install_kind = "windows-cuda",
|
||||
runtime_line = "cuda12",
|
||||
coverage_class = None,
|
||||
supported_sms = [],
|
||||
min_sm = None,
|
||||
max_sm = None,
|
||||
bundle_profile = None,
|
||||
)
|
||||
],
|
||||
release_tag = "llama-prebuilt-latest",
|
||||
upstream_tag = "b9000",
|
||||
assets = {asset_name: f"https://published.example/{asset_name}"},
|
||||
)
|
||||
checksums = make_checksums_with_source(
|
||||
[asset_name],
|
||||
release_tag = release.release_tag,
|
||||
upstream_tag = "b9000",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_resolved_published_releases",
|
||||
lambda requested_tag, published_repo, published_release_tag = "": iter(
|
||||
[
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = release,
|
||||
checksums = checksums,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"github_release_assets",
|
||||
lambda repo, tag: (_ for _ in ()).throw(
|
||||
AssertionError(
|
||||
"published Windows CUDA choice should not query upstream"
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
requested_tag, resolved_tag, attempts, approved = resolve_install_attempts(
|
||||
"latest",
|
||||
host,
|
||||
"unslothai/llama.cpp",
|
||||
"",
|
||||
)
|
||||
|
||||
assert requested_tag == "latest"
|
||||
assert resolved_tag == "b9000"
|
||||
assert attempts[0].name == asset_name
|
||||
assert attempts[0].source_label == "published"
|
||||
assert attempts[0].expected_sha256 == "a" * 64
|
||||
assert approved.release_tag == "llama-prebuilt-latest"
|
||||
|
||||
def test_windows_cuda_uses_selected_release_upstream_tag(self, monkeypatch):
|
||||
host = make_host(system = "Windows", machine = "AMD64")
|
||||
host.driver_cuda_version = (12, 4)
|
||||
mock_windows_runtime(monkeypatch, ["cuda12"])
|
||||
release = make_release(
|
||||
[], release_tag = "llama-prebuilt-latest", upstream_tag = "b9000"
|
||||
)
|
||||
checksums = make_checksums_with_source(
|
||||
["llama-b9000-bin-win-cuda-12.4-x64.zip"],
|
||||
release_tag = release.release_tag,
|
||||
upstream_tag = "b9000",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_resolved_published_releases",
|
||||
lambda requested_tag, published_repo, published_release_tag = "": iter(
|
||||
[
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = release,
|
||||
checksums = checksums,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"github_release_assets",
|
||||
lambda repo, tag: {
|
||||
f"llama-{tag}-bin-win-cuda-12.4-x64.zip": f"https://example.com/llama-{tag}-bin-win-cuda-12.4-x64.zip"
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"resolve_windows_cuda_choices",
|
||||
lambda host, tag, assets: [
|
||||
AssetChoice(
|
||||
repo = UPSTREAM_REPO,
|
||||
tag = tag,
|
||||
name = f"llama-{tag}-bin-win-cuda-12.4-x64.zip",
|
||||
url = assets[f"llama-{tag}-bin-win-cuda-12.4-x64.zip"],
|
||||
source_label = "upstream",
|
||||
install_kind = "windows-cuda",
|
||||
runtime_line = "cuda12",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
requested_tag, resolved_tag, attempts, approved = resolve_install_attempts(
|
||||
"latest",
|
||||
host,
|
||||
"unslothai/llama.cpp",
|
||||
"",
|
||||
)
|
||||
|
||||
assert requested_tag == "latest"
|
||||
assert resolved_tag == "b9000"
|
||||
assert attempts[0].name == "llama-b9000-bin-win-cuda-12.4-x64.zip"
|
||||
assert attempts[0].expected_sha256 == "a" * 64
|
||||
assert approved.release_tag == "llama-prebuilt-latest"
|
||||
|
||||
def test_linux_cpu_uses_same_tag_upstream_asset(self, monkeypatch):
|
||||
host = make_host(
|
||||
has_usable_nvidia = False,
|
||||
has_physical_nvidia = False,
|
||||
nvidia_smi = None,
|
||||
)
|
||||
release = make_release(
|
||||
[], release_tag = "llama-prebuilt-latest", upstream_tag = "b9000"
|
||||
)
|
||||
checksums = make_checksums_with_source(
|
||||
["llama-b9000-bin-ubuntu-x64.tar.gz"],
|
||||
release_tag = release.release_tag,
|
||||
upstream_tag = "b9000",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_resolved_published_releases",
|
||||
lambda requested_tag, published_repo, published_release_tag = "": iter(
|
||||
[
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = release,
|
||||
checksums = checksums,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"github_release_assets",
|
||||
lambda repo, tag: {
|
||||
f"llama-{tag}-bin-ubuntu-x64.tar.gz": f"https://example.com/llama-{tag}-bin-ubuntu-x64.tar.gz"
|
||||
},
|
||||
)
|
||||
|
||||
_requested_tag, resolved_tag, attempts, _approved = resolve_install_attempts(
|
||||
"latest",
|
||||
host,
|
||||
"unslothai/llama.cpp",
|
||||
"",
|
||||
)
|
||||
|
||||
assert resolved_tag == "b9000"
|
||||
assert attempts[0].name == "llama-b9000-bin-ubuntu-x64.tar.gz"
|
||||
assert attempts[0].source_label == "upstream"
|
||||
assert attempts[0].expected_sha256 == "a" * 64
|
||||
|
||||
def test_linux_cuda_does_not_fall_back_to_upstream_cpu(self, monkeypatch):
|
||||
host = make_host(system = "Linux", machine = "x86_64", compute_caps = ["86"])
|
||||
release = make_release(
|
||||
[], release_tag = "llama-prebuilt-latest", upstream_tag = "b9000"
|
||||
)
|
||||
checksums = make_checksums_with_source(
|
||||
[],
|
||||
release_tag = release.release_tag,
|
||||
upstream_tag = "b9000",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_resolved_published_releases",
|
||||
lambda requested_tag, published_repo, published_release_tag = "": iter(
|
||||
[
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = release,
|
||||
checksums = checksums,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
mock_linux_runtime(monkeypatch, ["cuda12"])
|
||||
|
||||
with pytest.raises(
|
||||
PrebuiltFallback, match = "no compatible published Linux CUDA bundle"
|
||||
):
|
||||
resolve_install_attempts("latest", host, "unslothai/llama.cpp", "")
|
||||
|
||||
def test_windows_cpu_prefers_published_asset(self, monkeypatch):
|
||||
host = make_host(
|
||||
system = "Windows",
|
||||
machine = "AMD64",
|
||||
has_usable_nvidia = False,
|
||||
has_physical_nvidia = False,
|
||||
nvidia_smi = None,
|
||||
)
|
||||
asset_name = "llama-b9000-bin-win-cpu-x64.zip"
|
||||
release = make_release(
|
||||
[
|
||||
make_artifact(
|
||||
asset_name,
|
||||
install_kind = "windows-cpu",
|
||||
runtime_line = None,
|
||||
coverage_class = None,
|
||||
supported_sms = [],
|
||||
min_sm = None,
|
||||
max_sm = None,
|
||||
bundle_profile = None,
|
||||
)
|
||||
],
|
||||
release_tag = "llama-prebuilt-latest",
|
||||
upstream_tag = "b9000",
|
||||
assets = {asset_name: f"https://published.example/{asset_name}"},
|
||||
)
|
||||
checksums = make_checksums_with_source(
|
||||
[asset_name],
|
||||
release_tag = release.release_tag,
|
||||
upstream_tag = "b9000",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_resolved_published_releases",
|
||||
lambda requested_tag, published_repo, published_release_tag = "": iter(
|
||||
[
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = release,
|
||||
checksums = checksums,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"github_release_assets",
|
||||
lambda repo, tag: (_ for _ in ()).throw(
|
||||
AssertionError("published Windows CPU choice should not query upstream")
|
||||
),
|
||||
)
|
||||
|
||||
_requested_tag, resolved_tag, attempts, _approved = resolve_install_attempts(
|
||||
"latest",
|
||||
host,
|
||||
"unslothai/llama.cpp",
|
||||
"",
|
||||
)
|
||||
|
||||
assert resolved_tag == "b9000"
|
||||
assert attempts[0].name == asset_name
|
||||
assert attempts[0].source_label == "published"
|
||||
|
||||
def test_macos_prefers_published_asset(self, monkeypatch):
|
||||
host = make_host(
|
||||
system = "Darwin",
|
||||
machine = "arm64",
|
||||
nvidia_smi = None,
|
||||
driver_cuda_version = None,
|
||||
compute_caps = [],
|
||||
has_physical_nvidia = False,
|
||||
has_usable_nvidia = False,
|
||||
)
|
||||
asset_name = "llama-b9000-bin-macos-arm64.tar.gz"
|
||||
release = make_release(
|
||||
[
|
||||
make_artifact(
|
||||
asset_name,
|
||||
install_kind = "macos-arm64",
|
||||
runtime_line = None,
|
||||
coverage_class = None,
|
||||
supported_sms = [],
|
||||
min_sm = None,
|
||||
max_sm = None,
|
||||
bundle_profile = None,
|
||||
)
|
||||
],
|
||||
release_tag = "llama-prebuilt-latest",
|
||||
upstream_tag = "b9000",
|
||||
assets = {asset_name: f"https://published.example/{asset_name}"},
|
||||
)
|
||||
checksums = make_checksums_with_source(
|
||||
[asset_name],
|
||||
release_tag = release.release_tag,
|
||||
upstream_tag = "b9000",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_resolved_published_releases",
|
||||
lambda requested_tag, published_repo, published_release_tag = "": iter(
|
||||
[
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = release,
|
||||
checksums = checksums,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"github_release_assets",
|
||||
lambda repo, tag: (_ for _ in ()).throw(
|
||||
AssertionError("published macOS choice should not query upstream")
|
||||
),
|
||||
)
|
||||
|
||||
_requested_tag, resolved_tag, attempts, _approved = resolve_install_attempts(
|
||||
"latest",
|
||||
host,
|
||||
"unslothai/llama.cpp",
|
||||
"",
|
||||
)
|
||||
|
||||
assert resolved_tag == "b9000"
|
||||
assert attempts[0].name == asset_name
|
||||
assert attempts[0].source_label == "published"
|
||||
|
||||
def test_windows_cpu_missing_checksum_rejects_install(self, monkeypatch):
|
||||
host = make_host(
|
||||
system = "Windows",
|
||||
machine = "AMD64",
|
||||
has_usable_nvidia = False,
|
||||
has_physical_nvidia = False,
|
||||
nvidia_smi = None,
|
||||
)
|
||||
published_name = "llama-b9000-bin-win-cpu-x64.zip"
|
||||
release = make_release(
|
||||
[
|
||||
make_artifact(
|
||||
published_name,
|
||||
install_kind = "windows-cpu",
|
||||
runtime_line = None,
|
||||
coverage_class = None,
|
||||
supported_sms = [],
|
||||
min_sm = None,
|
||||
max_sm = None,
|
||||
bundle_profile = None,
|
||||
)
|
||||
],
|
||||
release_tag = "llama-prebuilt-latest",
|
||||
upstream_tag = "b9000",
|
||||
assets = {published_name: f"https://published.example/{published_name}"},
|
||||
)
|
||||
checksums = make_checksums_with_source(
|
||||
[],
|
||||
release_tag = release.release_tag,
|
||||
upstream_tag = "b9000",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_resolved_published_releases",
|
||||
lambda requested_tag, published_repo, published_release_tag = "": iter(
|
||||
[
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = release,
|
||||
checksums = checksums,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"github_release_assets",
|
||||
lambda repo, tag: {
|
||||
f"llama-{tag}-bin-win-cpu-x64.zip": f"https://upstream.example/llama-{tag}-bin-win-cpu-x64.zip"
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
PrebuiltFallback,
|
||||
match = "approved checksum asset did not contain the selected prebuilt archive",
|
||||
):
|
||||
resolve_install_attempts(
|
||||
"latest",
|
||||
host,
|
||||
"unslothai/llama.cpp",
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
class TestResolveInstallReleasePlans:
|
||||
def test_latest_collects_multiple_older_release_plans_up_to_limit(
|
||||
self, monkeypatch
|
||||
):
|
||||
host = make_host(
|
||||
has_usable_nvidia = False,
|
||||
has_physical_nvidia = False,
|
||||
nvidia_smi = None,
|
||||
)
|
||||
releases = [
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = make_release([], release_tag = "r3", upstream_tag = "b9003"),
|
||||
checksums = make_checksums_with_source(
|
||||
["llama-b9003-bin-ubuntu-x64.tar.gz"],
|
||||
release_tag = "r3",
|
||||
upstream_tag = "b9003",
|
||||
),
|
||||
),
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = make_release([], release_tag = "r2", upstream_tag = "b9002"),
|
||||
checksums = make_checksums_with_source(
|
||||
["llama-b9002-bin-ubuntu-x64.tar.gz"],
|
||||
release_tag = "r2",
|
||||
upstream_tag = "b9002",
|
||||
),
|
||||
),
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = make_release([], release_tag = "r1", upstream_tag = "b9001"),
|
||||
checksums = make_checksums_with_source(
|
||||
["llama-b9001-bin-ubuntu-x64.tar.gz"],
|
||||
release_tag = "r1",
|
||||
upstream_tag = "b9001",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_resolved_published_releases",
|
||||
lambda requested_tag, published_repo, published_release_tag = "": iter(
|
||||
releases
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"github_release_assets",
|
||||
lambda repo, tag: {
|
||||
f"llama-{tag}-bin-ubuntu-x64.tar.gz": f"https://example.com/llama-{tag}-bin-ubuntu-x64.tar.gz"
|
||||
},
|
||||
)
|
||||
|
||||
requested_tag, plans = resolve_install_release_plans(
|
||||
"latest",
|
||||
host,
|
||||
"unslothai/llama.cpp",
|
||||
"",
|
||||
max_release_fallbacks = 2,
|
||||
)
|
||||
|
||||
assert requested_tag == "latest"
|
||||
assert [plan.release_tag for plan in plans] == ["r3", "r2"]
|
||||
assert [plan.llama_tag for plan in plans] == ["b9003", "b9002"]
|
||||
|
||||
def test_latest_skips_non_installable_release_and_keeps_searching(
|
||||
self, monkeypatch
|
||||
):
|
||||
host = make_host(
|
||||
has_usable_nvidia = False,
|
||||
has_physical_nvidia = False,
|
||||
nvidia_smi = None,
|
||||
)
|
||||
releases = [
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = make_release([], release_tag = "r2", upstream_tag = "b9002"),
|
||||
checksums = make_checksums_with_source(
|
||||
[],
|
||||
release_tag = "r2",
|
||||
upstream_tag = "b9002",
|
||||
),
|
||||
),
|
||||
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
|
||||
bundle = make_release([], release_tag = "r1", upstream_tag = "b9001"),
|
||||
checksums = make_checksums_with_source(
|
||||
["llama-b9001-bin-ubuntu-x64.tar.gz"],
|
||||
release_tag = "r1",
|
||||
upstream_tag = "b9001",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"iter_resolved_published_releases",
|
||||
lambda requested_tag, published_repo, published_release_tag = "": iter(
|
||||
releases
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
INSTALL_LLAMA_PREBUILT,
|
||||
"github_release_assets",
|
||||
lambda repo, tag: (
|
||||
{}
|
||||
if tag == "b9002"
|
||||
else {
|
||||
f"llama-{tag}-bin-ubuntu-x64.tar.gz": f"https://example.com/llama-{tag}-bin-ubuntu-x64.tar.gz"
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
_requested_tag, plans = resolve_install_release_plans(
|
||||
"latest",
|
||||
host,
|
||||
"unslothai/llama.cpp",
|
||||
"",
|
||||
max_release_fallbacks = 2,
|
||||
)
|
||||
|
||||
assert len(plans) == 1
|
||||
assert plans[0].release_tag == "r1"
|
||||
assert plans[0].llama_tag == "b9001"
|
||||
|
||||
def test_malformed_release_fallback_env_uses_default(self, monkeypatch):
|
||||
monkeypatch.setenv("UNSLOTH_LLAMA_MAX_PREBUILT_RELEASE_FALLBACKS", "not-an-int")
|
||||
assert (
|
||||
env_int("UNSLOTH_LLAMA_MAX_PREBUILT_RELEASE_FALLBACKS", 3, minimum = 1) == 3
|
||||
)
|
||||
|
||||
def test_import_with_malformed_release_fallback_env_does_not_crash(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("UNSLOTH_LLAMA_MAX_PREBUILT_RELEASE_FALLBACKS", "bad-value")
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"studio_install_llama_prebuilt_env_reload",
|
||||
MODULE_PATH,
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
assert module.DEFAULT_MAX_PREBUILT_RELEASE_FALLBACKS == 2
|
||||
finally:
|
||||
sys.modules.pop(spec.name, None)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# N. windows_cuda_attempts
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
|
|
@ -753,7 +1430,7 @@ class TestWindowsCudaAttempts:
|
|||
|
||||
|
||||
# ===========================================================================
|
||||
# L. resolve_upstream_asset_choice -- platform routing
|
||||
# O. resolve_upstream_asset_choice -- platform routing
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
|
|
|
|||
175
tests/studio/install/test_validate_llama_prebuilt.py
Normal file
175
tests/studio/install/test_validate_llama_prebuilt.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
MODULE_PATH = REPO_ROOT / "validate-llama-prebuilt.py"
|
||||
|
||||
if not MODULE_PATH.is_file():
|
||||
pytest.skip(
|
||||
f"validate-llama-prebuilt.py not present at {MODULE_PATH}",
|
||||
allow_module_level = True,
|
||||
)
|
||||
|
||||
SPEC = importlib.util.spec_from_file_location("validate_llama_prebuilt", MODULE_PATH)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
VALIDATE = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = VALIDATE
|
||||
SPEC.loader.exec_module(VALIDATE)
|
||||
|
||||
|
||||
def test_build_local_approved_checksums_uses_staged_upstream_tag(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
stage_dir = tmp_path / "release-1"
|
||||
stage_dir.mkdir()
|
||||
asset_path = stage_dir / "app-test-linux-x64-cuda12-newer.tar.gz"
|
||||
asset_path.write_bytes(b"bundle")
|
||||
sibling_checksums = stage_dir / VALIDATE.installer.DEFAULT_PUBLISHED_SHA256_ASSET
|
||||
sibling_checksums.write_text(
|
||||
"""
|
||||
{
|
||||
"schema_version": 1,
|
||||
"component": "llama.cpp",
|
||||
"release_tag": "release-1",
|
||||
"upstream_tag": "b9001",
|
||||
"source_commit": "deadbeef",
|
||||
"artifacts": {
|
||||
"llama.cpp-source-b9001.tar.gz": {
|
||||
"sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"kind": "upstream-source"
|
||||
}
|
||||
}
|
||||
}
|
||||
""".strip()
|
||||
+ "\n",
|
||||
encoding = "utf-8",
|
||||
)
|
||||
asset = VALIDATE.LocalAsset(
|
||||
path = asset_path,
|
||||
tag = "test",
|
||||
name = asset_path.name,
|
||||
install_kind = "linux-cuda",
|
||||
source_kind = "app-bundle",
|
||||
native_runnable = True,
|
||||
bundle_profile = "cuda12-newer",
|
||||
runtime_line = "cuda12",
|
||||
)
|
||||
|
||||
checksums = VALIDATE.build_local_approved_checksums(
|
||||
asset,
|
||||
allow_network_source_hash = False,
|
||||
)
|
||||
|
||||
assert checksums.release_tag == "release-1"
|
||||
assert checksums.upstream_tag == "b9001"
|
||||
assert "llama.cpp-source-b9001.tar.gz" in checksums.artifacts
|
||||
assert "llama.cpp-source-test.tar.gz" not in checksums.artifacts
|
||||
|
||||
|
||||
def test_validate_native_asset_passes_release_tag_and_upstream_tag(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
stage_dir = tmp_path / "release-7"
|
||||
stage_dir.mkdir()
|
||||
asset_path = stage_dir / "app-test-linux-x64-cuda12-newer.tar.gz"
|
||||
asset_path.write_bytes(b"bundle")
|
||||
sibling_checksums = stage_dir / VALIDATE.installer.DEFAULT_PUBLISHED_SHA256_ASSET
|
||||
sibling_checksums.write_text(
|
||||
"""
|
||||
{
|
||||
"schema_version": 1,
|
||||
"component": "llama.cpp",
|
||||
"release_tag": "release-7",
|
||||
"upstream_tag": "b9007",
|
||||
"source_commit": "deadbeef",
|
||||
"artifacts": {
|
||||
"llama.cpp-source-b9007.tar.gz": {
|
||||
"sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"kind": "upstream-source"
|
||||
}
|
||||
}
|
||||
}
|
||||
""".strip()
|
||||
+ "\n",
|
||||
encoding = "utf-8",
|
||||
)
|
||||
asset = VALIDATE.LocalAsset(
|
||||
path = asset_path,
|
||||
tag = "test",
|
||||
name = asset_path.name,
|
||||
install_kind = "linux-cuda",
|
||||
source_kind = "app-bundle",
|
||||
native_runnable = True,
|
||||
bundle_profile = "cuda12-newer",
|
||||
runtime_line = "cuda12",
|
||||
)
|
||||
|
||||
host = VALIDATE.installer.HostInfo(
|
||||
system = "Linux",
|
||||
machine = "x86_64",
|
||||
is_windows = False,
|
||||
is_linux = True,
|
||||
is_macos = False,
|
||||
is_x86_64 = True,
|
||||
is_arm64 = False,
|
||||
nvidia_smi = None,
|
||||
driver_cuda_version = None,
|
||||
compute_caps = [],
|
||||
visible_cuda_devices = None,
|
||||
has_physical_nvidia = False,
|
||||
has_usable_nvidia = False,
|
||||
)
|
||||
monkeypatch.setattr(VALIDATE.installer, "detect_host", lambda: host)
|
||||
monkeypatch.setattr(
|
||||
VALIDATE.installer,
|
||||
"download_validation_model",
|
||||
lambda probe_path, cache_path: probe_path.write_bytes(b"probe"),
|
||||
)
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_validate_prebuilt_attempts(
|
||||
attempts,
|
||||
host,
|
||||
install_dir,
|
||||
work_dir,
|
||||
probe_path,
|
||||
*,
|
||||
requested_tag,
|
||||
llama_tag,
|
||||
release_tag,
|
||||
approved_checksums,
|
||||
initial_fallback_used = False,
|
||||
existing_install_dir = None,
|
||||
):
|
||||
captured["requested_tag"] = requested_tag
|
||||
captured["llama_tag"] = llama_tag
|
||||
captured["release_tag"] = release_tag
|
||||
staging_dir = VALIDATE.installer.create_install_staging_dir(install_dir)
|
||||
return attempts[0], staging_dir, False
|
||||
|
||||
monkeypatch.setattr(
|
||||
VALIDATE.installer,
|
||||
"validate_prebuilt_attempts",
|
||||
fake_validate_prebuilt_attempts,
|
||||
)
|
||||
|
||||
record = VALIDATE.validate_native_asset(
|
||||
asset,
|
||||
keep_temp = False,
|
||||
allow_network_source_hash = False,
|
||||
)
|
||||
|
||||
assert record.status == "PASS"
|
||||
assert captured == {
|
||||
"requested_tag": "test",
|
||||
"llama_tag": "b9007",
|
||||
"release_tag": "release-7",
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue