Fix native GGUF context ceiling and guard picker template reads

Restore the native context store field so the sidebar slider keeps the
full ceiling for drag and drop GGUFs. Limit local chat template reads to
the browse allowlist, skip malformed repo ids, and drop unused model
picker exports.
This commit is contained in:
sneakr 2026-07-09 20:46:43 +02:00
commit f82327c8c1
11 changed files with 42 additions and 36 deletions

View file

@ -6,12 +6,17 @@ from __future__ import annotations
import json
import logging
import os
import re
from pathlib import Path
from typing import Optional
from jinja2 import TemplateError
from jinja2.sandbox import ImmutableSandboxedEnvironment
from hub.services.models.folder_browser import (
_build_browse_allowlist,
_is_path_inside_allowlist,
)
from utils.models.gguf_metadata import read_gguf_chat_template
from utils.models.model_config import (
_extract_quant_label,
@ -22,6 +27,7 @@ from utils.models.model_config import (
from utils.paths.path_utils import (
get_cache_path,
is_local_path,
normalize_path,
resolve_cached_repo_id_case,
)
@ -29,6 +35,12 @@ from .schemas import ValidateChatTemplateResponse
logger = logging.getLogger(__name__)
_VALID_REPO_ID = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
def _is_valid_repo_id(repo_id: str) -> bool:
return bool(_VALID_REPO_ID.fullmatch(repo_id))
_TOKENIZER_CONFIG_PATHS = ("tokenizer_config.json", "LLM/tokenizer_config.json")
_JINJA_TEMPLATE_PATHS = ("chat_template.jinja", "LLM/chat_template.jinja")
_PROCESSOR_TEMPLATE_PATHS = ("chat_template.json", "LLM/chat_template.json")
@ -221,13 +233,20 @@ def read_default_chat_template(
if is_local_path(name):
try:
target = Path(normalize_path(name)).expanduser()
if not _is_path_inside_allowlist(target, _build_browse_allowlist()):
logger.debug("Refused chat template read outside allowed folders: %s", name)
return None
if name.lower().endswith(".gguf"):
return read_gguf_chat_template(name)
return _chat_template_from_dir(Path(name), gguf_variant)
return read_gguf_chat_template(str(target))
return _chat_template_from_dir(target, gguf_variant)
except Exception as exc:
logger.debug("Could not read local chat template for %s: %s", name, exc)
return None
if not _is_valid_repo_id(name):
return None
resolved = resolve_cached_repo_id_case(name)
try:

View file

@ -1211,6 +1211,9 @@ export function ChatPage({
const ggufContextLength = useChatRuntimeStore(
(state) => state.ggufContextLength,
);
const ggufNativeContextLength = useChatRuntimeStore(
(state) => state.ggufNativeContextLength,
);
const contextUsage = useChatRuntimeStore((state) => state.contextUsage);
const modelsFromStore = useChatRuntimeStore((state) => state.models);
const lorasFromStore = useChatRuntimeStore((state) => state.loras);
@ -1987,6 +1990,7 @@ export function ChatPage({
activeGgufVariant: null,
ggufContextLength: null,
ggufMaxContextLength: null,
ggufNativeContextLength: null,
activeNativePathToken: null,
// Clear previous-model counters, else the relaxed external-provider
// render gate shows stale stats until the next completion.
@ -2720,6 +2724,7 @@ export function ChatPage({
modelId={inferenceParams.checkpoint}
ggufVariant={activeGgufVariant ?? null}
isGguf={activeModelIsGguf}
nativeContextLength={ggufNativeContextLength}
loadedContextLength={ggufContextLength}
loadedConfig={activeModelConfig}
onReload={handleReloadActiveModel}

View file

@ -700,6 +700,9 @@ export function useChatModelRuntime() {
const reportedMaxCtx = loadResponse.is_gguf
? (loadResponse.max_context_length ?? null)
: null;
const reportedNativeCtx = loadResponse.is_gguf
? (loadResponse.native_context_length ?? null)
: null;
// A successful reload has applied settings, so clear pending custom
// context state and display the backend-reported effective context.
const keepCustomCtx = null;
@ -732,6 +735,7 @@ export function useChatModelRuntime() {
useChatRuntimeStore.setState({
ggufContextLength: nativeCtx,
ggufMaxContextLength,
ggufNativeContextLength: reportedNativeCtx,
modelRequiresTrustRemoteCode:
loadResponse.requires_trust_remote_code ?? false,
supportsReasoning,

View file

@ -165,6 +165,9 @@ export function applyActiveModelStatusToStore(
const ggufMaxContextLength = status.is_gguf
? (status.max_context_length ?? null)
: null;
const ggufNativeContextLength = status.is_gguf
? (status.native_context_length ?? null)
: null;
const currentSpecType = normalizeSpeculativeType(status.speculative_type);
const prevState = useChatRuntimeStore.getState();
const clampedReasoningEffort =
@ -199,6 +202,7 @@ export function applyActiveModelStatusToStore(
: true,
ggufContextLength: currentGgufContextLength,
ggufMaxContextLength,
ggufNativeContextLength,
...(status.is_gguf ? {} : { activeNativePathToken: null }),
modelRequiresTrustRemoteCode: status.requires_trust_remote_code ?? false,
defaultChatTemplate: nextDefaultChatTemplate,

View file

@ -493,6 +493,7 @@ type ChatRuntimeStore = {
activeGgufVariant: string | null;
ggufContextLength: number | null;
ggufMaxContextLength: number | null;
ggufNativeContextLength: number | null;
modelRequiresTrustRemoteCode: boolean;
supportsReasoning: boolean;
reasoningAlwaysOn: boolean;
@ -955,6 +956,7 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
activeGgufVariant: null,
ggufContextLength: null,
ggufMaxContextLength: null,
ggufNativeContextLength: null,
modelRequiresTrustRemoteCode: false,
supportsReasoning: false,
reasoningAlwaysOn: false,
@ -1210,6 +1212,7 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
activeNativePathToken: null,
ggufContextLength: null,
ggufMaxContextLength: null,
ggufNativeContextLength: null,
modelRequiresTrustRemoteCode: false,
contextUsage: null,
supportsReasoning: false,

View file

@ -118,7 +118,6 @@ export type {
ExternalModelOption,
LoraModelOption,
ModelOption,
ModelPickTarget,
ModelSelectorChangeMeta,
} from "./model-selector/types";

View file

@ -27,7 +27,7 @@ import type {
CachedModelRepo,
LocalModelInfo,
} from "@/features/chat/api/chat-api";
import { useChatPickerInventory } from "@/features/model-picker/inventory/use-chat-picker-inventory";
import { useChatPickerInventory } from "../../inventory/use-chat-picker-inventory";
import type { GgufVariantDetail } from "@/features/chat/types/api";
import { DotTag } from "@/features/hub/catalog/dot-tag";
import {

View file

@ -10,6 +10,7 @@ interface SidebarModelConfigProps {
modelId: string;
ggufVariant: string | null;
isGguf: boolean;
nativeContextLength: number | null;
loadedContextLength: number | null;
loadedConfig: PerModelConfig;
onReload: (config: PerModelConfig) => void;
@ -52,6 +53,7 @@ export function SidebarModelConfig({
modelId,
ggufVariant,
isGguf,
nativeContextLength,
loadedContextLength,
loadedConfig,
onReload,
@ -69,10 +71,10 @@ export function SidebarModelConfig({
ggufVariant: ggufVariant ?? undefined,
isGguf,
isDownloaded: true,
contextLength: null,
contextLength: nativeContextLength,
},
};
}, [modelId, ggufVariant, isGguf]);
}, [modelId, ggufVariant, isGguf, nativeContextLength]);
return (
<ModelConfigPage

View file

@ -8,12 +8,8 @@ export type {
ExternalModelOption,
LoraModelOption,
ModelOption,
ModelPickTarget,
ModelSelectorChangeMeta,
} from "./components/model-selector";
export { FolderBrowser } from "./components/model-selector/folder-browser";
export type { FolderBrowserProps } from "./components/model-selector/folder-browser";
export { ModelDeleteAction } from "./components/model-selector/model-delete-action";
export {
applyModelLoadConfigToRuntime,
applyPerModelConfigToRuntime,
@ -21,10 +17,7 @@ export {
perModelConfigsEqual,
} from "./model-config/apply-per-model-config";
export {
DEFAULT_PER_MODEL_CONFIG,
normalizeMaxSeqLength,
type PerModelConfig,
deletePerModelConfig,
deletePerModelConfigsForModel,
resolveInitialConfig,
} from "./model-config/per-model-config";

View file

@ -7,8 +7,6 @@ import {
} from "@/features/hub/lib/model-identity";
export {
ggufVariantsMatch,
modelIdsMatch,
normalizeGgufVariantIdentity,
normalizeModelIdentity,
} from "@/features/hub/lib/model-identity";

View file

@ -9,8 +9,6 @@ import {
normalizeModelIdentity,
} from "./model-identity";
export { modelStorageKey as configEntryKey };
export interface PerModelConfig {
customContextLength: number | null;
maxSeqLength: number | null;
@ -557,25 +555,6 @@ export function deletePerModelConfig(
}
}
export function deletePerModelConfigsForModel(modelId: string): void {
const map = readMap();
const targetIdentity = normalizeModelIdentity(modelId);
let changed = false;
for (const key of Object.keys(map)) {
const storedModelId = modelIdFromStorageKey(key);
if (
storedModelId &&
normalizeModelIdentity(storedModelId) === targetIdentity
) {
delete map[key];
changed = true;
}
}
if (changed) {
writeMap(map);
}
}
export function resolveInitialConfig(
modelId: string,
ggufVariant?: string | null,