mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 18:35:04 -10:00
Models: GPU numbering matches the other screens, friendly model names, fit figures less what the kept models use
Owner-approved for 0.2.0. The card list says (GPU n) with the same numbers as Monitor and the picker; a loaded model is named from the catalog (Qwen3.8-Flash-Next Q2_0) in the picker, the Loaded list, the sidebar card and the Monitor, the raw file id only as a fallback; with Run multiple models on, This computer and the card rows show what is left after the models that stay loaded (a short muted line says so), in 8 languages. The picker rows no longer print the raw id beside the name.
This commit is contained in:
1 parent
6149cec03e
commit
201d674e94
15 files changed
+80
-18
No files matched your search
@@ -0,0 +1,8 @@
|
||||
/** The name a person reads for a loaded model: the family and the version from the catalog ("Qwen3.8-Flash-Next Q2_0"), not the file id. */
|
||||
export function friendlyModelName(catalog, id) {
|
||||
for (const family of catalog?.families ?? []) {
|
||||
const size = (family.sizes ?? []).find(s => s.id === id);
|
||||
if (size?.model && family.title) return `${String(family.title).replace(/\s*\(Gyro\)\s*$/, '')} ${size.model}`.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { plainError, PHASE_TEXT } from './update-messages.mjs';
|
||||
import { checkRunConfig, describeIssues } from './run-config-check.mjs';
|
||||
import { openTextFile } from './open-text-file.mjs';
|
||||
import { markEngine, engineRefusal } from './engine-version.mjs';
|
||||
import { friendlyModelName } from './model-names.mjs';
|
||||
|
||||
const ROOT=resolve(dirname(fileURLToPath(import.meta.url)),'..');
|
||||
const safeId=value=>typeof value==='string'&&/^[a-z0-9_-]{1,80}$/i.test(value);
|
||||
@@ -222,7 +223,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
if(!args.multiple){if(busy(requests().filter(r=>instances.has(r.instanceId))))throw new Error('Finish or cancel native requests before switching models.');for(const id of [...instances.keys()])await unload(id);}else if(matching)await unload(matching.id);
|
||||
if(job.state!=='running')return;
|
||||
writeFileSync(configPath,JSON.stringify(cfg,null,2),{mode:0o600});
|
||||
const instance={id,apiModelId,fingerprint,context:entry.context??0,aliases:cfg.aliases??[],vision:!!cfg.vision,resourceProfile:cfg.ui_resource_profile??null,model:args.model,label:entry.model_name??args.model,port:cfg.port,gpus:Array.isArray(cfg.gpu)?cfg.gpu:cfg.gpu===undefined?[]:[cfg.gpu],status:'loading',phase:'loading',parallel:cfg.parallel??1,mcpServers:cfg.mcp_servers??cfg.mcpServers??{},key,configPath,loadedAt:new Date().toISOString(),metrics:null,error:null};
|
||||
const instance={id,apiModelId,fingerprint,context:entry.context??0,aliases:cfg.aliases??[],vision:!!cfg.vision,resourceProfile:cfg.ui_resource_profile??null,model:args.model,label:friendlyModelName(snapshot.catalog,args.model)??entry.model_name??args.model,port:cfg.port,gpus:Array.isArray(cfg.gpu)?cfg.gpu:cfg.gpu===undefined?[]:[cfg.gpu],status:'loading',phase:'loading',parallel:cfg.parallel??1,mcpServers:cfg.mcp_servers??cfg.mcpServers??{},key,configPath,loadedAt:new Date().toISOString(),metrics:null,error:null};
|
||||
/* Another model on the same graphics card: the engine takes the card's free memory, so a second one usually finds none. Loading goes on (recommend, never force); if it fails, the reason is said. */
|
||||
const sharedWith=instance.gpus.length?[...instances.values()].find(i=>i.id!==id&&i.status!=='error'&&i.model!==args.model&&i.gpus.some(g=>instance.gpus.includes(g))):null;
|
||||
const sharedSentence=sharedWith?`GPU ${instance.gpus.find(g=>sharedWith.gpus.includes(g))+1} is already used by ${sharedWith.label}, so this model did not fit there. Load it on another GPU, or unload ${sharedWith.label} first.`:null;
|
||||
|
||||
@@ -20,7 +20,7 @@ function NativeChatModelPicker() {
|
||||
const [open, setOpen] = useState(false), [error, setError] = useState('');
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const current = native?.instances.find(i => i.id === native.defaultId);
|
||||
return <><button className="chat-model-badge chat-model-picker" ref={ref} aria-label={tr("Choose model: {0}", [current?.label ?? 'Native model'])} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(v => !v)}><Icon name="models" size={16}/><span>{tr(current?.label ?? 'Native model')}</span><Icon name="chevron" size={12}/></button>{tr(open ? <FloatingMenu anchor={ref.current} title={tr("Chat model")} width={340} onClose={() => setOpen(false)}>{tr(native?.instances.filter(i => i.status !== 'error').map(i => <button className="menu__item" role="menuitemradio" aria-checked={current?.id === i.id} disabled={streaming || i.status !== 'ready'} key={i.id} onClick={() => void nativeAction(`instances/${i.id}/default`).then(() => setOpen(false)).catch(e => setError(e.message))}><Icon name="models"/><span><strong>{tr(i.label)}</strong><small>{tr(i.apiModelId)} · {tr(i.gpus.map(g => `GPU ${g + 1}`).join(' + ') || 'Automatic GPU')}</small></span></button>))}{tr(providers?.connections.flatMap(c => c.models.map(m => <button className="menu__item" role="menuitem" disabled={streaming} key={`${c.id}-${m.id}`} onClick={() => { selectRemoteModel({ connectionId: c.id, modelId: m.id }); setOpen(false); }}><Icon name="link"/>{m.name} · {c.name}</button>)))}<button className="menu__item" role="menuitem" onClick={() => { setOpen(false); go('models'); }}>{tr("Manage models")}</button>{tr(error ? <p className="small inline-error">{tr(error)}</p> : null)}</FloatingMenu> : null)}</>;
|
||||
return <><button className="chat-model-badge chat-model-picker" ref={ref} aria-label={tr("Choose model: {0}", [current?.label ?? 'Native model'])} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(v => !v)}><Icon name="models" size={16}/><span>{tr(current?.label ?? 'Native model')}</span><Icon name="chevron" size={12}/></button>{tr(open ? <FloatingMenu anchor={ref.current} title={tr("Chat model")} width={340} onClose={() => setOpen(false)}>{tr(native?.instances.filter(i => i.status !== 'error').map(i => <button className="menu__item" role="menuitemradio" aria-checked={current?.id === i.id} disabled={streaming || i.status !== 'ready'} key={i.id} onClick={() => void nativeAction(`instances/${i.id}/default`).then(() => setOpen(false)).catch(e => setError(e.message))}><Icon name="models"/><span><strong>{tr(i.label)}</strong><small>{tr(i.gpus.map(g => `GPU ${g + 1}`).join(" + ") || "Automatic GPU")}</small></span></button>))}{tr(providers?.connections.flatMap(c => c.models.map(m => <button className="menu__item" role="menuitem" disabled={streaming} key={`${c.id}-${m.id}`} onClick={() => { selectRemoteModel({ connectionId: c.id, modelId: m.id }); setOpen(false); }}><Icon name="link"/>{m.name} · {c.name}</button>)))}<button className="menu__item" role="menuitem" onClick={() => { setOpen(false); go('models'); }}>{tr("Manage models")}</button>{tr(error ? <p className="small inline-error">{tr(error)}</p> : null)}</FloatingMenu> : null)}</>;
|
||||
}
|
||||
function LegacyChatModelPicker() {
|
||||
const native = useNative();
|
||||
@@ -77,7 +77,7 @@ function LegacyChatModelPicker() {
|
||||
return <>
|
||||
<button ref={trigger} className="chat-model-badge chat-model-picker" aria-label={tr("Choose model: {0}", [apiModel?.name ?? (running?.local ? running.name : running?.family) ?? 'No model loaded'])} title={tr(apiModel ? `${connection?.name} · ${apiModel.id}` : 'View and switch models')} disabled={!library && !providers} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(v => !v)}><Icon name={apiModel ? 'link' : modelFamilyIcon(running?.family ?? groups[0]?.family)} size={16}/><span>{tr(apiModel?.name ?? (running?.local ? running.name : running?.family) ?? groups[0]?.family ?? 'No model loaded')}</span><Icon name="chevron" size={12}/></button>
|
||||
{tr(open ? <FloatingMenu anchor={trigger.current} title={tr("Models")} width={380} onClose={() => setOpen(false)}>
|
||||
{tr(native?.instances.filter(i => i.status !== 'error').map(i => <button key={i.id} className="menu__item" role="menuitem" disabled={streaming || i.status !== 'ready'} onClick={() => void nativeAction(`instances/${i.id}/default`).then(() => { selectRemoteModel(null); setOpen(false); }).catch(e => setError(e.message))}><Icon name="models"/><span>{tr(i.label)}<small>{tr(i.apiModelId)}</small></span></button>))}<p className="model-picker-heading">{tr("Local models")}</p>
|
||||
{tr(native?.instances.filter(i => i.status !== 'error').map(i => <button key={i.id} className="menu__item" role="menuitem" disabled={streaming || i.status !== 'ready'} onClick={() => void nativeAction(`instances/${i.id}/default`).then(() => { selectRemoteModel(null); setOpen(false); }).catch(e => setError(e.message))}><Icon name="models"/><span>{tr(i.label)}<small>{tr(i.gpus.map(g => `GPU ${g + 1}`).join(" + ") || "Automatic GPU")}</small></span></button>))}<p className="model-picker-heading">{tr("Local models")}</p>
|
||||
{tr(!groups.length ? <p className="model-picker-empty">{tr("No local models downloaded yet.")}</p> : null)}
|
||||
{tr(groups.map(({ family, versions }, index) => {
|
||||
const main = versions[0], state = pickerVersionState(main, library!.loaded);
|
||||
|
||||
@@ -50,7 +50,12 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
// This computer for the fit figures: the biggest usable graphics card, or the ones chosen under Expert settings.
|
||||
const pcGpus = (native?.hardware?.gpus ?? []).filter(g => g.usable !== false), pcGpu = [...pcGpus].sort((x, y) => y.vram_gb - x.vram_gb)[0] ?? null;
|
||||
const pcRam = native?.hardware?.ram_gb || hardware?.memory.totalGb || null, pcVram = pcGpu?.vram_gb ?? 0;
|
||||
const fitHardware = { ram_gb: pcRam, vram_gb: pcVram };
|
||||
// What the models that stay loaded (Run multiple models) already take: the fit figures are for what is left.
|
||||
const sizeOf = (id: string) => allSizes.find(s => s.id === id);
|
||||
const keptModels = expert && multiple ? (native?.instances ?? []).filter(i => i.status !== 'error' && i.model !== selectedSize?.id) : [];
|
||||
const keptUse = keptModels.map(i => { const s = sizeOf(i.model); const card = pcGpus.find(g => g.index === i.gpus[0]); const f = s && pcRam ? fitOn(s, { ram_gb: pcRam, vram_gb: card?.vram_gb ?? pcVram }) : null; return { gpus: i.gpus, ram: f?.ramUseGb ?? 0, vram: f?.vramUseGb ?? 0 }; });
|
||||
const keptRam = keptUse.reduce((n, u) => n + u.ram, 0);
|
||||
const keptVramOn = (cards: number[]) => keptUse.filter(u => u.gpus.some(g => cards.includes(g))).reduce((n, u) => n + u.vram, 0);
|
||||
const loadedSelected = native?.instances.find(i => i.model === selectedSize?.id && i.status === 'ready');
|
||||
// A model that is already on this PC loads as it was prepared: the page shows its settings, so loading it never asks to download.
|
||||
useEffect(() => { if (installed?.ready) {
|
||||
@@ -81,6 +86,12 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
const cardOptions = selectedSize?.gpu_options && selectedSize.gpu_options.options.length >= 2 ? selectedSize.gpu_options : null;
|
||||
const activeCards = cardOptions ? (cardOptions.options.find(o => o.key === cardsKey) ?? (multiple ? cardOptions.options.find(o => o.recommended && !usedBy(o.cards).length) ?? cardOptions.options.find(o => !usedBy(o.cards).length) : undefined) ?? cardOptions.options.find(o => o.recommended) ?? cardOptions.options[0]) : null;
|
||||
const cardsGpus: number[] = activeCards?.cards ?? [];
|
||||
const chosenCards = expert && gpus.length ? gpus : cardsGpus;
|
||||
const fitHardware = { ram_gb: pcRam ? Math.max(0, pcRam - keptRam) : pcRam, vram_gb: Math.max(0, pcVram - keptVramOn(chosenCards)) };
|
||||
// "(card 1)" in a card name is the number the screens call GPU 2 (Monitor, the model picker, the loaded list): one numbering everywhere.
|
||||
const gpuNumbers = (text: string) => text.replace(/\(card (\d+)\)/g, (_m, n) => `(GPU ${Number(n) + 1})`);
|
||||
// The fit sentence for a card choice, less what the kept models take of RAM and of that card's memory.
|
||||
const lessKept = (words: string, cards: number[]) => { const gone = Math.round(keptRam + keptVramOn(cards)); return gone > 0 ? words.replace(/of the (\d+) GB available/, (_m, n) => `of the ${Math.max(0, Number(n) - gone)} GB available`) : words; };
|
||||
useEffect(() => { setCardsKey(null); }, [selectedSize?.id]);
|
||||
const values = useMemo(() => ({ family: selectedSize?.sourceFamily, model: selectedSize?.model, context, vision: selectedSize?.images ? vision : 'no', kv, low_ram: lowRam, resourceProfile, ...(expert && gpus.length ? { gpus: gpus.join(',') } : cardsGpus.length > 1 ? { gpus: cardsGpus.join(',') } : cardsGpus.length === 1 ? { gpu: cardsGpus[0] } : {}), loadAfter: true, multiple: expert && multiple }), [selectedSize?.sourceFamily, selectedSize?.model, context, vision, kv, lowRam, resourceProfile, gpus.join(','), cardsGpus.join(','), expert, multiple]);
|
||||
const downloadValues = { ...values, vision: selectedSize?.images ? 'yes' : 'no', loadVision: selectedSize?.images ? vision : 'no' };
|
||||
@@ -147,7 +158,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Conversation context")}</span><Select ariaLabel={tr("Conversation context")} value={String(context)} options={(native?.catalog?.contexts ?? [8192, 32768, 65536, 131072, 262144]).map(n => ({ value: String(n), label: `${n / 1024}K` }))} onChange={n => setContext(Number(n))}/></div>
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Computer use")}</span><div className="native-resource-choice"><button aria-pressed={resourceProfile === 'work'} data-selected={resourceProfile === 'work'} onClick={() => setResourceProfile('work')}><Icon name="computer" size={20}/><span><strong>{tr("Leave 20% for my work")}</strong><small>{tr("Headroom for your other apps")}</small></span>{tr(resourceProfile === 'work' ? <Icon name="check" size={15}/> : null)}</button><button aria-pressed={resourceProfile === 'performance'} data-selected={resourceProfile === 'performance'} onClick={() => setResourceProfile('performance')}><Icon name="bolt" size={20}/><span><strong>{tr("Full performance")}</strong><small>{tr("Give Strata priority")}</small></span>{tr(resourceProfile === 'performance' ? <Icon name="check" size={15}/> : null)}</button></div></div>
|
||||
</div>
|
||||
{tr(cardOptions ? <div className="native-cards"><span className="native-choice__label">{tr("Graphics cards")}</span><div className="native-cards__list" role="radiogroup" aria-label={tr("Graphics cards")}>{cardOptions.options.map(o => { const on = activeCards?.key === o.key; return <button key={o.key} type="button" role="radio" aria-checked={on} data-selected={on} disabled={!!active} className="native-cards__opt" onClick={() => setCardsKey(o.key)}><span className="native-cards__dot" aria-hidden="true"/><span><strong>{o.label}</strong><small data-warn={o.verdict === 'spills' || o.verdict === 'tight' || usedBy(o.cards).length > 0}>{[o.words, ...o.notes, ...(usedBy(o.cards).length ? [tr('Already used by {0}', [usedBy(o.cards).join(', ')])] : [])].join('. ')}.</small></span></button>; })}{cardOptions.ignored.length ? <p className="small muted native-cards__note">{`Not used: ${cardOptions.ignored.map(g => `${g.name ?? 'a card'} (${g.why_ignored})`).join('; ')}.`}</p> : null}</div></div> : null)}
|
||||
{tr(cardOptions ? <div className="native-cards"><span className="native-choice__label">{tr("Graphics cards")}</span><div className="native-cards__list" role="radiogroup" aria-label={tr("Graphics cards")}>{cardOptions.options.map(o => { const on = activeCards?.key === o.key; return <button key={o.key} type="button" role="radio" aria-checked={on} data-selected={on} disabled={!!active} className="native-cards__opt" onClick={() => setCardsKey(o.key)}><span className="native-cards__dot" aria-hidden="true"/><span><strong>{gpuNumbers(o.label)}</strong><small data-warn={o.verdict === 'spills' || o.verdict === 'tight' || usedBy(o.cards).length > 0}>{[lessKept(o.words, o.cards), ...o.notes, ...(usedBy(o.cards).length ? [tr('Already used by {0}', [usedBy(o.cards).join(', ')])] : [])].join('. ')}.</small></span></button>; })}{cardOptions.ignored.length ? <p className="small muted native-cards__note">{`Not used: ${cardOptions.ignored.map(g => `${g.name ?? 'a card'} (${g.why_ignored})`).join('; ')}.`}</p> : null}</div></div> : null)}
|
||||
{tr(selectedSize?.imagesCapable && !selectedSize.images ? <p className="small muted native-spill-note">{tr("This model can read pictures, but the engine installed here does not include the picture reader yet, so Read pictures is off. Update the engine in Settings → About to turn it on.")}</p> : null)}
|
||||
{tr(selectedSize?.images ? <><div className="native-image-reading"><label><input type="checkbox" checked={vision !== 'no'} disabled={!selectedSize?.images || !!active} onChange={e => setVision(e.target.checked ? 'yes' : 'no')}/><span><strong>{tr("Read pictures ")}<small>{tr("Optional")}</small></strong><small>{tr("Understand photos, screenshots and charts. Vision files download automatically. Enable them when loading this model.")}</small></span></label>{tr(expert && vision !== 'no' ? <Select ariaLabel={tr("Image encoder device")} value={vision === 'yes' ? 'yes' : vision} options={[{ value: 'yes', label: 'Automatic' }, { value: 'gpu', label: 'GPU' }, { value: 'cpu', label: 'CPU' }]} onChange={setVision}/> : null)}</div>{tr(vision !== 'no' && installed?.ready && installed.visionPrepared === false ? <div className="native-vision-missing" role="status"><span>{tr("Picture reading needs a few more files that are not on this computer yet.")}</span><Btn size="sm" disabled={pending || !!active} onClick={() => void download()}>{tr("Download picture-reading files")}</Btn></div> : null)}</> : null)}
|
||||
</> : null)}
|
||||
@@ -160,7 +171,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
{tr(expert && IMAGE_GENERATION_VISIBLE ? <><details className="native-generation"><summary>{tr("Image generation ")}<span className="muted">{tr("Optional")}</span></summary><RunImageModel library={imageLibrary} disabled={pending || !!active} onChange={() => { }}/></details></> : null)}
|
||||
{tr(expert ? <section id="native-expert-settings" ref={expertSection} className="st-card native-execution"><h2 className="h4">{tr("Expert settings")}</h2>{tr(tunedNow ? <p className="small muted native-tuned-note">{tr("Showing the values of your tuning '{0}'.", [tunedNow.name])}</p> : null)}<label className="native-toggle"><span><strong>{tr("Run multiple models")}</strong><small>{tr("Loading another model keeps the others loaded.")}</small></span><button className="st-toggle" role="switch" aria-label={tr("Run multiple models")} aria-checked={multiple} onClick={() => setMultiple(v => !v)}/></label><h3 className="small strong">{tr("Run on")}</h3><p className="small muted">{tr("Automatic uses the installation\u2019s GPU placement. Choose a GPU, or split this model across several GPUs.")}</p><Select ariaLabel={tr("Run on")} value={deviceChoice} options={[{ value: 'auto', label: 'Automatic' }, ...(native?.hardware?.gpus ?? []).map(g => ({ value: String(g.index), label: `GPU ${g.index + 1} · ${g.name}` })), ...((native?.hardware?.gpus.length ?? 0) > 1 ? [{ value: 'split', label: 'Split across GPUs…' }] : [])]} onChange={value => { setDeviceChoice(value); setGpus(value === 'auto' ? [] : value === 'split' ? (native?.hardware?.gpus ?? []).map(g => g.index) : [Number(value)]); }}/>{tr(deviceChoice === 'split' ? <div className="native-gpu-options">{tr(native?.hardware?.gpus.map(g => <label key={g.index}><input type="checkbox" checked={gpus.includes(g.index)} onChange={e => setGpus(old => e.target.checked ? [...old, g.index] : old.filter(i => i !== g.index))}/><span>{tr("GPU ")}{tr(g.index + 1)} · {tr(g.name)}<small>{tr(g.vram_gb)}{tr(" GB VRAM")}</small></span></label>))}</div> : null)}{tr(gpus.length > 1 ? <label className="input-field"><span>{tr("Layer split")}</span><input className="st-input" value={split} placeholder={tr("auto or 16,32")} onChange={e => setSplit(e.target.value)}/></label> : null)}<div className="native-field-grid"><label className="input-field"><span>{tr("Context memory")}</span><Select ariaLabel={tr("Native KV format")} value={kv} options={[{ value: 'int8', label: 'INT8' }, { value: 'q4_0', label: 'Q4' }, { value: 'k8v4', label: 'K8 / V4' }]} onChange={setKv}/></label><label className="input-field"><span>{tr("Low-RAM mode")}</span><Select ariaLabel={tr("Native low RAM mode")} value={lowRam} options={['auto', 'resident', 'mmap'].map(v => ({ value: v, label: v === 'auto' ? 'Automatic' : v === 'resident' ? 'Keep experts in RAM' : 'Read experts from disk' }))} onChange={setLowRam}/></label></div>{tr(installed ? <><NativeSettingsFile model={installed.model}/><NativeSettings model={installed.model}/></> : <p className="small muted">{tr("More engine parameters become available after preparing this version.")}</p>)}</section> : null)}
|
||||
</div>
|
||||
<aside className="stack stack--lg"><PcFitCard gpuName={pcGpu?.name} ramGb={pcRam} vramGb={pcVram} fit={selectedSize ? fitOn(selectedSize, fitHardware) : null}/><section className="st-card"><h2 className="h4">{tr("Model files")}</h2><p className="small muted">{tr("Strata prepares all shards, the draft layer and shared files. Completed downloads are reused.")}</p><code className="native-path">{installed?.dataFolder ?? native?.downloadDirectory ?? native?.installed?.data_folder ?? 'Choose an installation'}</code><Btn size="sm" variant="ghost" onClick={() => { setSettingsSection('Storage'); go('settings'); }}>{tr("Storage settings")}</Btn></section><Btn size="sm" variant="ghost" onClick={onOtherModels}>{tr("Other local models\u2026")}</Btn></aside>
|
||||
<aside className="stack stack--lg"><PcFitCard gpuName={pcGpu?.name} ramGb={pcRam} vramGb={pcVram} inUse={keptModels.length ? { ramGb: Math.round(keptRam), vramGb: Math.round(keptVramOn(chosenCards)) } : null} fit={selectedSize ? fitOn(selectedSize, fitHardware) : null}/><section className="st-card"><h2 className="h4">{tr("Model files")}</h2><p className="small muted">{tr("Strata prepares all shards, the draft layer and shared files. Completed downloads are reused.")}</p><code className="native-path">{installed?.dataFolder ?? native?.downloadDirectory ?? native?.installed?.data_folder ?? 'Choose an installation'}</code><Btn size="sm" variant="ghost" onClick={() => { setSettingsSection('Storage'); go('settings'); }}>{tr("Storage settings")}</Btn></section><Btn size="sm" variant="ghost" onClick={onOtherModels}>{tr("Other local models\u2026")}</Btn></aside>
|
||||
</div>
|
||||
|
||||
<Dialog open={!!plan} title={tr(installed?.ready ? "Apply settings and load" : "Download and prepare")} compact onClose={() => !pending && setPlan(null)} actions={<><Btn disabled={pending} onClick={() => setPlan(null)}>{tr("Cancel")}</Btn><Btn variant="primary" disabled={pending || !!plan?.plan?.disk_short} onClick={() => void download()}>{tr(installed?.ready ? "Apply & load" : "Download & load")}</Btn></>}><p><strong>{tr(plan?.plan?.title ?? selectedFamily?.title)}</strong>{tr(installed?.ready ? null : <>{tr(" \u00B7 approximately ")}{tr(plan?.plan?.download_gb ?? selectedSize?.download_gb)}{tr(" GB")}</>)}</p>{tr(installed?.ready ? <p className="small muted">{tr("The model files are already on this computer. Nothing will be downloaded; only the settings you chose are applied.")}</p> : null)}{tr(plan?.plan?.disk_short ? <ErrorNotice message={plan.plan.disk_short}/> : null)}{tr(installed?.ready ? <p className="small muted">{tr("Any currently loaded native models will be unloaded first.")}</p> : <><p className="small muted">{tr("Existing files are checked and reused. Any currently loaded native models will be unloaded before preparing files.")}</p><p className="small muted">{tr("Your computer may feel slower while weights load. You can stop the download and resume later.")}</p></>)}{tr(selectedSize?.images ? <p className="small muted">{tr("Vision encoder files are included. Picture reading loads only when enabled.")}</p> : null)}</Dialog>
|
||||
|
||||
@@ -15,12 +15,14 @@ function Meter({ label, total, use }: { label: string; total: number; use: numbe
|
||||
* "This computer": the graphics memory and RAM, and what the chosen version would take of each. One plain line says whether it fits
|
||||
* or how much would be read from the SSD (the n-gram table, which always sits on the SSD, is not counted).
|
||||
*/
|
||||
export function PcFitCard({ gpuName, ramGb, vramGb, fit }: { gpuName?: string | null; ramGb: number | null; vramGb: number; fit: Fit | null }) {
|
||||
export function PcFitCard({ gpuName, ramGb, vramGb, fit, inUse = null }: { gpuName?: string | null; ramGb: number | null; vramGb: number; fit: Fit | null; inUse?: { ramGb: number; vramGb: number } | null }) {
|
||||
const freeVram = Math.max(0, vramGb - (inUse?.vramGb ?? 0)), freeRam = ramGb ? Math.max(0, ramGb - (inUse?.ramGb ?? 0)) : 0;
|
||||
return <section className="st-card pc-card">
|
||||
<div><h2 className="h4">{tr("This computer")}</h2>
|
||||
<p className="small muted">{tr(gpuName ? tr("{0} · {1} graphics memory", [gpuName, gb(vramGb)]) : tr("No supported graphics card reported by this installation."))}{ramGb ? tr(" · {0} RAM", [gb(ramGb)]) : ''}</p></div>
|
||||
{tr(fit?.known && vramGb > 0 ? <Meter label="Graphics memory" total={vramGb} use={fit.vramUseGb}/> : null)}
|
||||
{tr(fit?.known && ramGb ? <Meter label="RAM" total={ramGb} use={fit.ramUseGb}/> : null)}
|
||||
{tr(fit?.known && vramGb > 0 ? <Meter label="Graphics memory" total={freeVram} use={fit.vramUseGb}/> : null)}
|
||||
{tr(fit?.known && ramGb ? <Meter label="RAM" total={freeRam} use={fit.ramUseGb}/> : null)}
|
||||
{tr(inUse ? <p className="xs muted">{tr("Models that stay loaded use about {0} GB of RAM and {1} GB of this card’s memory.", [inUse.ramGb, inUse.vramGb])}</p> : null)}
|
||||
{tr(fit?.known ? <p className="pc-note">{tr(fit.spillGb >= 1 ? 'About {0} GB will be read from the SSD, so answers can be a little slower.' : 'Fits fully in memory.', [Math.round(fit.spillGb)])}</p> : null)}
|
||||
</section>;
|
||||
}
|
||||
@@ -26,4 +26,4 @@ export function scopedMonitor(live:MonitorState|null,runtime:RuntimeModels|null,
|
||||
totals:{...live.totals,tokensIn:count('tokensIn'),tokensOut:count('tokensOut'),requests:done.length,cachedTokens:0,uncachedTokens:count('tokensIn'),promptMs:undefined,decodeMs:undefined,avgTps:0}
|
||||
};
|
||||
}
|
||||
export const instanceLabel=(i:RuntimeModel)=>`${i.family} · ${i.name}${i.memory.context>0?` · ${i.memory.context/1024}K`:""}`;
|
||||
export const instanceLabel=(i:RuntimeModel)=>`${i.family}${i.family.toLowerCase().endsWith(i.name.toLowerCase())?"":` · ${i.name}`}${i.memory.context>0?` · ${i.memory.context/1024}K`:""}`;
|
||||
@@ -3197,5 +3197,6 @@
|
||||
"s3201": "cloudflared läuft bereits für andere Tunnel (PIDs {0}).",
|
||||
"s3202": "Der Strata-Tunnel für diese Adresse läuft bereits (PID {0}).",
|
||||
"s3203": "Der Strata-Tunnel für diese Adresse läuft bereits.",
|
||||
"s3204": "Beende ihn zuerst oder nutze ihn, wie er ist, und verbinde dich dann hier erneut."
|
||||
"s3204": "Beende ihn zuerst oder nutze ihn, wie er ist, und verbinde dich dann hier erneut.",
|
||||
"s3205": "Modelle, die geladen bleiben, belegen etwa {0} GB RAM und {1} GB des Speichers dieser Karte."
|
||||
}
|
||||
@@ -3197,5 +3197,6 @@
|
||||
"s3201": "cloudflared is already running for other tunnels (PIDs {0}).",
|
||||
"s3202": "Strata’s tunnel for this address is already running (PID {0}).",
|
||||
"s3203": "Strata’s tunnel for this address is already running.",
|
||||
"s3204": "Stop it first, or use it as it is, then connect again here."
|
||||
"s3204": "Stop it first, or use it as it is, then connect again here.",
|
||||
"s3205": "Models that stay loaded use about {0} GB of RAM and {1} GB of this card’s memory."
|
||||
}
|
||||
@@ -3197,5 +3197,6 @@
|
||||
"s3201": "cloudflared ya se está ejecutando para otros túneles (PID {0}).",
|
||||
"s3202": "El túnel de Strata para esta dirección ya se está ejecutando (PID {0}).",
|
||||
"s3203": "El túnel de Strata para esta dirección ya se está ejecutando.",
|
||||
"s3204": "Deténlo primero, o úsalo tal como está, y vuelve a conectar aquí."
|
||||
"s3204": "Deténlo primero, o úsalo tal como está, y vuelve a conectar aquí.",
|
||||
"s3205": "Los modelos que siguen cargados usan unos {0} GB de RAM y {1} GB de la memoria de esta tarjeta."
|
||||
}
|
||||
@@ -3197,5 +3197,6 @@
|
||||
"s3201": "cloudflared は他のトンネル用にすでに動作しています(PID {0})。",
|
||||
"s3202": "このアドレス用のStrataのトンネルはすでに動作しています(PID {0})。",
|
||||
"s3203": "このアドレス用のStrataのトンネルはすでに動作しています。",
|
||||
"s3204": "先にそれを停止するか、そのまま使ってから、ここで再度接続してください。"
|
||||
"s3204": "先にそれを停止するか、そのまま使ってから、ここで再度接続してください。",
|
||||
"s3205": "読み込んだままのモデルが、RAMを約 {0} GB、このカードのメモリを約 {1} GB 使用します。"
|
||||
}
|
||||
@@ -3197,5 +3197,6 @@
|
||||
"s3201": "cloudflared가 이미 다른 터널용으로 실행 중입니다(PID {0}).",
|
||||
"s3202": "이 주소용 Strata 터널이 이미 실행 중입니다(PID {0}).",
|
||||
"s3203": "이 주소용 Strata 터널이 이미 실행 중입니다.",
|
||||
"s3204": "먼저 중지하거나 그대로 사용한 뒤 여기서 다시 연결하세요."
|
||||
"s3204": "먼저 중지하거나 그대로 사용한 뒤 여기서 다시 연결하세요.",
|
||||
"s3205": "계속 불러 둔 모델이 RAM 약 {0} GB와 이 카드 메모리 약 {1} GB를 사용합니다."
|
||||
}
|
||||
@@ -3197,5 +3197,6 @@
|
||||
"s3201": "O cloudflared já está em execução para outros túneis (PIDs {0}).",
|
||||
"s3202": "O túnel do Strata para este endereço já está em execução (PID {0}).",
|
||||
"s3203": "O túnel do Strata para este endereço já está em execução.",
|
||||
"s3204": "Pare-o primeiro, ou use-o como está, e conecte novamente aqui."
|
||||
"s3204": "Pare-o primeiro, ou use-o como está, e conecte novamente aqui.",
|
||||
"s3205": "Os modelos que continuam carregados usam cerca de {0} GB de RAM e {1} GB da memória desta placa."
|
||||
}
|
||||
@@ -3197,5 +3197,6 @@
|
||||
"s3201": "cloudflared уже запущен для других туннелей (PID {0}).",
|
||||
"s3202": "Туннель Strata для этого адреса уже запущен (PID {0}).",
|
||||
"s3203": "Туннель Strata для этого адреса уже запущен.",
|
||||
"s3204": "Сначала остановите его или используйте как есть, затем подключитесь здесь снова."
|
||||
"s3204": "Сначала остановите его или используйте как есть, затем подключитесь здесь снова.",
|
||||
"s3205": "Модели, которые остаются загруженными, занимают около {0} ГБ ОЗУ и {1} ГБ памяти этой карты."
|
||||
}
|
||||
@@ -3197,5 +3197,6 @@
|
||||
"s3201": "cloudflared 已在为其他隧道运行(PID {0})。",
|
||||
"s3202": "这个地址的 Strata 隧道已在运行(PID {0})。",
|
||||
"s3203": "这个地址的 Strata 隧道已在运行。",
|
||||
"s3204": "请先停止它,或直接使用它,然后在这里重新连接。"
|
||||
"s3204": "请先停止它,或直接使用它,然后在这里重新连接。",
|
||||
"s3205": "保持加载的模型大约占用 {0} GB 内存和这块显卡 {1} GB 的显存。"
|
||||
}
|
||||
@@ -35,3 +35,35 @@ test('a model that failed to start is not offered in the chat model picker', asy
|
||||
const picker = await readFile(new URL('../src/components/ChatModelPicker.tsx', import.meta.url), 'utf8');
|
||||
assert.equal((picker.match(/native\?\.instances\.filter\(i => i\.status !== 'error'\)\.map/g) ?? []).length, 2);
|
||||
});
|
||||
|
||||
import { friendlyModelName } from '../server/model-names.mjs';
|
||||
test('a loaded model is named from the catalog (family and version), the file id only as a fallback', async () => {
|
||||
const catalog = { families: [{ title: 'Qwen3.8-Flash-Next', sizes: [{ id: 'q2_0', model: 'Q2_0' }] }, { title: 'Qwen3.8-Flash-Next (Gyro)', sizes: [{ id: 'gyro-gyro-s', model: 'Gyro-S' }] }] };
|
||||
assert.equal(friendlyModelName(catalog, 'q2_0'), 'Qwen3.8-Flash-Next Q2_0');
|
||||
assert.equal(friendlyModelName(catalog, 'gyro-gyro-s'), 'Qwen3.8-Flash-Next Gyro-S');
|
||||
assert.equal(friendlyModelName(catalog, 'nope'), null);
|
||||
assert.equal(friendlyModelName(null, 'q2_0'), null);
|
||||
const f = nativeFixture();
|
||||
try {
|
||||
await f.control.refresh(true);
|
||||
const done = await finishJob(f.control, f.control.operation('load', { model: 'iq2_xs' }).id);
|
||||
assert.equal(done.state, 'done', done.error);
|
||||
assert.equal(f.control.state().instances[0].label, 'Qwen3.8-Flash-Next IQ2_XS');
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
|
||||
test('card names use the GPU numbering of the other screens, and the fit figures subtract the models that stay loaded', async () => {
|
||||
const models = await readFile(new URL('../src/components/NativeModels.tsx', import.meta.url), 'utf8');
|
||||
assert.ok(models.includes('(GPU ${Number(n) + 1})') && models.includes('text.replace(/\\(card (\\d+)\\)/g'));
|
||||
assert.match(models, /<strong>\{gpuNumbers\(o\.label\)\}<\/strong>/);
|
||||
assert.match(models, /const keptModels = expert && multiple/);
|
||||
assert.match(models, /inUse=\{keptModels\.length \?/);
|
||||
const card = await readFile(new URL('../src/components/PcFitCard.tsx', import.meta.url), 'utf8');
|
||||
assert.match(card, /total=\{freeVram\}/);
|
||||
assert.match(card, /total=\{freeRam\}/);
|
||||
// the arithmetic itself
|
||||
const text = 'fits: needs 44 GB of the 273 GB available.';
|
||||
const gone = Math.round(14 + 27);
|
||||
assert.equal(text.replace(/of the (\d+) GB available/, (_m, n) => `of the ${Math.max(0, Number(n) - gone)} GB available`), 'fits: needs 44 GB of the 232 GB available.');
|
||||
assert.equal('RX 9070 (card 0) + RAM'.replace(/\(card (\d+)\)/g, (_m, n) => `(GPU ${Number(n) + 1})`), 'RX 9070 (GPU 1) + RAM');
|
||||
});
|
||||
Reference in new issue
Block a user