Studio: self-heal unsloth namespace shadows; clearer failed-load messages (#6532)
* Studio: self-heal unsloth namespace-package shadows in all subprocess workers A directory named `unsloth` (or `unsloth_zoo`) without an __init__.py on PYTHONPATH/sys.path, a stray source checkout or a polluted PYTHONPATH, makes `import unsloth` resolve to an empty namespace package, so a worker's `from unsloth import FastLanguageModel` dies with a cryptic "cannot import name ... (unknown location)". The LLM training path already recovered from this via `_ensure_real_packages` in trainer.py (PR #6269), but the inference, export, and embedding-training subprocesses imported Unsloth directly with no guard. Extract that helper into a shared, dependency-free core/import_guards.py and call it before the Unsloth import in every subprocess: it drops the offending sys.path entries, imports the real packages (unsloth before unsloth_zoo so the pre-zoo GPU fixes run), then restores sys.path. trainer.py now imports the shared helper instead of its local copy. Covers both unsloth and unsloth_zoo and both namespace origin forms (None and "namespace"). The existing PR #6269 test now exercises the shared helper. * Studio: distinguish a failed model load from no model in the attach gates A failed load never sets the checkpoint, so the image and audio attach gates fell through to "Load a model before adding images/audio", which reads as if the user simply forgot to pick a model rather than that the load errored. Add a dedicated lastModelLoadError to the chat runtime store, set only when an actual load attempt fails (not on refresh, list, status, or unload errors, which keep using modelsError) and cleared when the next load starts. The image gate (all three call sites) and the audio gate now use it to report a failed load and point at the server logs, while still blocking in exactly the same cases. * Tighten namespace-shadow guard and load-error comments
This commit is contained in:
parent
49d4c61623
commit
cba73457df
13 changed files with 113 additions and 65 deletions
|
|
@ -1926,6 +1926,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
externalModelLabel: externalSelection?.modelId ?? null,
|
||||
loadedIsMultimodal: runtime.loadedIsMultimodal,
|
||||
modelLoaded: !!params.checkpoint && !runtime.modelLoading,
|
||||
loadError: runtime.lastModelLoadError,
|
||||
});
|
||||
if (imageGateReason) {
|
||||
toast.error(imageGateReason);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ export class AudioAttachmentAdapter implements AttachmentAdapter {
|
|||
const modelLoaded = !!checkpoint && !state.modelLoading;
|
||||
let unavailableReason: string | null = null;
|
||||
if (!modelLoaded) {
|
||||
unavailableReason = "Load a model before adding audio files.";
|
||||
// Mirror the image gate: flag a failed load vs "no model picked".
|
||||
unavailableReason = state.lastModelLoadError
|
||||
? "The last model failed to load. Check the server logs, then load a model before adding audio files."
|
||||
: "Load a model before adding audio files.";
|
||||
} else if (!activeModel?.hasAudioInput) {
|
||||
const label = activeModel?.name || checkpoint || "Current model";
|
||||
unavailableReason = `${label} cannot accept audio. Load an audio-input model before attaching audio files.`;
|
||||
|
|
|
|||
|
|
@ -246,6 +246,9 @@ export function useChatModelRuntime() {
|
|||
const setLoras = useChatRuntimeStore((state) => state.setLoras);
|
||||
const setParams = useChatRuntimeStore((state) => state.setParams);
|
||||
const setModelsError = useChatRuntimeStore((state) => state.setModelsError);
|
||||
const setLastModelLoadError = useChatRuntimeStore(
|
||||
(state) => state.setLastModelLoadError,
|
||||
);
|
||||
const setCheckpoint = useChatRuntimeStore((state) => state.setCheckpoint);
|
||||
const clearCheckpoint = useChatRuntimeStore((state) => state.clearCheckpoint);
|
||||
|
||||
|
|
@ -493,6 +496,7 @@ export function useChatModelRuntime() {
|
|||
.filter(Boolean)
|
||||
.join(" ");
|
||||
setModelsError(null);
|
||||
setLastModelLoadError(null); // clear prior failed-load marker
|
||||
setLoadToastDismissedState(false);
|
||||
const loadInfo = {
|
||||
id: modelId,
|
||||
|
|
@ -1184,6 +1188,7 @@ export function useChatModelRuntime() {
|
|||
const message =
|
||||
error instanceof Error ? error.message : "Failed to load model";
|
||||
setModelsError(message);
|
||||
setLastModelLoadError(message); // load-specific failure for the attach gates
|
||||
if (throwOnError) {
|
||||
throw error instanceof Error ? error : new Error(message);
|
||||
}
|
||||
|
|
@ -1199,6 +1204,7 @@ export function useChatModelRuntime() {
|
|||
resetLoadingUi,
|
||||
setLoadToastDismissedState,
|
||||
setModelsError,
|
||||
setLastModelLoadError,
|
||||
setParams,
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ class VisionImageAdapter implements AttachmentAdapter {
|
|||
externalModelLabel,
|
||||
loadedIsMultimodal: state.loadedIsMultimodal,
|
||||
modelLoaded,
|
||||
loadError: state.lastModelLoadError,
|
||||
});
|
||||
if (unavailableReason) {
|
||||
toast.error(unavailableReason);
|
||||
|
|
|
|||
|
|
@ -485,6 +485,7 @@ export function SharedComposer({
|
|||
const modelLoaded = useChatRuntimeStore(
|
||||
(s) => !!s.params.checkpoint && !s.modelLoading,
|
||||
);
|
||||
const lastModelLoadError = useChatRuntimeStore((s) => s.lastModelLoadError);
|
||||
const loadedIsMultimodal = useChatRuntimeStore((s) => s.loadedIsMultimodal);
|
||||
const supportsReasoning = useChatRuntimeStore((s) => s.supportsReasoning);
|
||||
const reasoningAlwaysOn = useChatRuntimeStore((s) => s.reasoningAlwaysOn);
|
||||
|
|
@ -566,6 +567,7 @@ export function SharedComposer({
|
|||
externalModelLabel: externalSelection?.modelId ?? null,
|
||||
loadedIsMultimodal,
|
||||
modelLoaded,
|
||||
loadError: lastModelLoadError,
|
||||
});
|
||||
const isCompareMode = Boolean(model1?.id || model2?.id);
|
||||
// Attach-time gate. Compare mode defers to send: the catalog can lag a
|
||||
|
|
|
|||
|
|
@ -482,6 +482,9 @@ type ChatRuntimeStore = {
|
|||
autoTitle: boolean;
|
||||
hfToken: string;
|
||||
modelsError: string | null;
|
||||
// Set only when a LOAD fails (not refresh/list/unload, which use modelsError);
|
||||
// lets the attach gates flag a failed load vs "no model picked".
|
||||
lastModelLoadError: string | null;
|
||||
activeGgufVariant: string | null;
|
||||
ggufContextLength: number | null;
|
||||
ggufMaxContextLength: number | null;
|
||||
|
|
@ -660,6 +663,7 @@ type ChatRuntimeStore = {
|
|||
setAutoTitle: (enabled: boolean) => void;
|
||||
setHfToken: (token: string) => void;
|
||||
setModelsError: (error: string | null) => void;
|
||||
setLastModelLoadError: (error: string | null) => void;
|
||||
setCheckpoint: (modelId: string, ggufVariant?: string | null) => void;
|
||||
setActiveThreadId: (threadId: string | null) => void;
|
||||
setActiveProjectId: (projectId: string | null) => void;
|
||||
|
|
@ -964,6 +968,7 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
|
|||
autoTitle: false,
|
||||
hfToken: loadString(HF_TOKEN_KEY, ""),
|
||||
modelsError: null,
|
||||
lastModelLoadError: null,
|
||||
activeGgufVariant: null,
|
||||
ggufContextLength: null,
|
||||
ggufMaxContextLength: null,
|
||||
|
|
@ -1159,6 +1164,7 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
|
|||
notifyHfTokenChanged(hfToken);
|
||||
},
|
||||
setModelsError: (modelsError) => set({ modelsError }),
|
||||
setLastModelLoadError: (lastModelLoadError) => set({ lastModelLoadError }),
|
||||
setCheckpoint: (modelId, ggufVariant) =>
|
||||
set((state) => {
|
||||
// Persist external selections so they survive a refresh. Local ids are
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export function getImageInputUnavailableReason({
|
|||
externalModelLabel,
|
||||
loadedIsMultimodal,
|
||||
modelLoaded,
|
||||
loadError,
|
||||
}: {
|
||||
activeModel?: ChatModelSummary;
|
||||
isExternalModel: boolean;
|
||||
|
|
@ -21,6 +22,8 @@ export function getImageInputUnavailableReason({
|
|||
externalModelLabel?: string | null;
|
||||
loadedIsMultimodal: boolean;
|
||||
modelLoaded: boolean;
|
||||
// Runtime lastModelLoadError; lets the no-model branch flag a failed load.
|
||||
loadError?: string | null;
|
||||
}): string | null {
|
||||
if (isExternalModel) {
|
||||
const explicitlyNonVision =
|
||||
|
|
@ -39,7 +42,13 @@ export function getImageInputUnavailableReason({
|
|||
}
|
||||
return null;
|
||||
}
|
||||
if (!modelLoaded) return "Load a model before adding images.";
|
||||
if (!modelLoaded) {
|
||||
// Distinguish a failed load from "no model picked yet".
|
||||
if (loadError) {
|
||||
return "The last model failed to load. Check the server logs, then load a model before adding images.";
|
||||
}
|
||||
return "Load a model before adding images.";
|
||||
}
|
||||
// loadedIsMultimodal is true for vision OR audio; that one flag can't tell
|
||||
// them apart, so only block when activeModel confirms audio-only (audio
|
||||
// capability set AND isVision === false). Otherwise trust the load
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue