From f82327c8c11e7f2ba5219386b841d8bd69f384ae Mon Sep 17 00:00:00 2001 From: sneakr Date: Thu, 9 Jul 2026 20:46:43 +0200 Subject: [PATCH] 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. --- studio/backend/picker/service.py | 23 +++++++++++++++++-- .../frontend/src/features/chat/chat-page.tsx | 5 ++++ .../chat/hooks/use-chat-model-runtime.ts | 4 ++++ .../lib/apply-inference-status-to-store.ts | 4 ++++ .../chat/stores/chat-runtime-store.ts | 3 +++ .../components/model-selector.tsx | 1 - .../components/model-selector/pickers.tsx | 2 +- .../components/sidebar-model-config.tsx | 6 +++-- .../src/features/model-picker/index.ts | 7 ------ .../model-config/model-identity.ts | 2 -- .../model-config/per-model-config.ts | 21 ----------------- 11 files changed, 42 insertions(+), 36 deletions(-) diff --git a/studio/backend/picker/service.py b/studio/backend/picker/service.py index 009c375113..4706196d2d 100644 --- a/studio/backend/picker/service.py +++ b/studio/backend/picker/service.py @@ -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: diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index d574d811ab..3b2f043255 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -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} diff --git a/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts b/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts index 7137e402cd..3dd7ad55b5 100644 --- a/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts +++ b/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts @@ -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, diff --git a/studio/frontend/src/features/chat/lib/apply-inference-status-to-store.ts b/studio/frontend/src/features/chat/lib/apply-inference-status-to-store.ts index c1b602cf32..129454bca7 100644 --- a/studio/frontend/src/features/chat/lib/apply-inference-status-to-store.ts +++ b/studio/frontend/src/features/chat/lib/apply-inference-status-to-store.ts @@ -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, diff --git a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts index f4b039f0d6..742625afc6 100644 --- a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts +++ b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts @@ -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((set, get) => ({ activeGgufVariant: null, ggufContextLength: null, ggufMaxContextLength: null, + ggufNativeContextLength: null, modelRequiresTrustRemoteCode: false, supportsReasoning: false, reasoningAlwaysOn: false, @@ -1210,6 +1212,7 @@ export const useChatRuntimeStore = create((set, get) => ({ activeNativePathToken: null, ggufContextLength: null, ggufMaxContextLength: null, + ggufNativeContextLength: null, modelRequiresTrustRemoteCode: false, contextUsage: null, supportsReasoning: false, diff --git a/studio/frontend/src/features/model-picker/components/model-selector.tsx b/studio/frontend/src/features/model-picker/components/model-selector.tsx index 0b8e1acd23..2470ab84ad 100644 --- a/studio/frontend/src/features/model-picker/components/model-selector.tsx +++ b/studio/frontend/src/features/model-picker/components/model-selector.tsx @@ -118,7 +118,6 @@ export type { ExternalModelOption, LoraModelOption, ModelOption, - ModelPickTarget, ModelSelectorChangeMeta, } from "./model-selector/types"; diff --git a/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx b/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx index 8c7c94d529..d470cbc8cf 100644 --- a/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx +++ b/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx @@ -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 { diff --git a/studio/frontend/src/features/model-picker/components/sidebar-model-config.tsx b/studio/frontend/src/features/model-picker/components/sidebar-model-config.tsx index 7c4fb18eb4..c7887c4357 100644 --- a/studio/frontend/src/features/model-picker/components/sidebar-model-config.tsx +++ b/studio/frontend/src/features/model-picker/components/sidebar-model-config.tsx @@ -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 (