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 (