Studio: Fix chat template disappearing after browser refresh (#5209)

* fix: preserve chat template on refresh

* chore: simplify chat template status lookup
This commit is contained in:
Lee Jackson 2026-05-01 16:19:09 +01:00 committed by GitHub
commit 05f46686de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 43 additions and 0 deletions

View file

@ -1701,6 +1701,18 @@ class LlamaCppBackend:
if chat_template_override:
import tempfile
self._chat_template = chat_template_override
flags = detect_reasoning_flags(
self._chat_template,
self._model_identifier,
log_source = "GGUF chat template override",
)
self._supports_reasoning = flags["supports_reasoning"]
self._reasoning_style = flags["reasoning_style"]
self._reasoning_always_on = flags["reasoning_always_on"]
self._supports_preserve_thinking = flags["supports_preserve_thinking"]
self._supports_tools = flags["supports_tools"]
self._chat_template_file = tempfile.NamedTemporaryFile(
mode = "w",
suffix = ".jinja",

View file

@ -283,6 +283,10 @@ class InferenceStatusResponse(BaseModel):
supports_tools: bool = Field(
False, description = "Whether the active model supports tool calling"
)
chat_template: Optional[str] = Field(
None,
description = "Jinja2 chat template string for the active model",
)
context_length: Optional[int] = Field(
None, description = "Context length of the active model"
)

View file

@ -985,6 +985,7 @@ async def get_status(
reasoning_always_on = llama_backend.reasoning_always_on,
supports_preserve_thinking = llama_backend.supports_preserve_thinking,
supports_tools = llama_backend.supports_tools,
chat_template = llama_backend.chat_template,
context_length = llama_backend.context_length,
max_context_length = llama_backend.max_context_length,
native_context_length = llama_backend.native_context_length,
@ -998,12 +999,19 @@ async def get_status(
is_audio = False
audio_type = None
has_audio_input = False
model_info = {}
if backend.active_model_name:
model_info = backend.models.get(backend.active_model_name, {})
is_vision = model_info.get("is_vision", False)
is_audio = model_info.get("is_audio", False)
audio_type = model_info.get("audio_type")
has_audio_input = model_info.get("has_audio_input", False)
chat_template_info = model_info.get("chat_template_info", {})
chat_template = (
chat_template_info.get("template")
if isinstance(chat_template_info, dict)
else None
)
# Non-GGUF: only gpt-oss Harmony is wired through the transformers
# generation path. Other template-level reasoning / tool kwargs
@ -1041,6 +1049,7 @@ async def get_status(
reasoning_always_on = False,
supports_preserve_thinking = False,
supports_tools = False,
chat_template = chat_template,
)
except Exception as e:

View file

@ -320,11 +320,23 @@ class TestPydanticModels:
"""Field exists in InferenceStatusResponse.model_fields."""
assert "native_context_length" in InferenceStatusResponse.model_fields
def test_status_response_has_chat_template_field(self):
"""Status includes chat_template so the UI can rehydrate after refresh."""
assert "chat_template" in InferenceStatusResponse.model_fields
def test_status_response_defaults_none(self):
"""Omitting native_context_length defaults to None."""
resp = InferenceStatusResponse()
assert resp.native_context_length is None
def test_status_response_chat_template_roundtrip(self):
"""chat_template serializes and validates as part of status."""
resp = InferenceStatusResponse(chat_template = "{{ messages }}")
roundtripped = InferenceStatusResponse.model_validate_json(
resp.model_dump_json()
)
assert roundtripped.chat_template == "{{ messages }}"
def test_roundtrip_preserves_value(self):
"""model_validate_json(model_dump_json()) round-trips."""
resp = LoadResponse(

View file

@ -266,6 +266,10 @@ export function useChatModelRuntime() {
? (statusRes.native_context_length ?? null)
: null;
const currentSpecType = normalizeSpeculativeType(statusRes.speculative_type);
const nextDefaultChatTemplate =
statusRes.chat_template === undefined
? useChatRuntimeStore.getState().defaultChatTemplate
: statusRes.chat_template;
useChatRuntimeStore.setState({
supportsReasoning,
reasoningAlwaysOn,
@ -282,6 +286,7 @@ export function useChatModelRuntime() {
ggufNativeContextLength,
modelRequiresTrustRemoteCode:
statusRes.requires_trust_remote_code ?? false,
defaultChatTemplate: nextDefaultChatTemplate,
speculativeType: currentSpecType,
loadedSpeculativeType: currentSpecType,
});

View file

@ -129,6 +129,7 @@ export interface InferenceStatusResponse {
reasoning_always_on?: boolean;
supports_preserve_thinking?: boolean;
supports_tools?: boolean;
chat_template?: string | null;
context_length?: number | null;
max_context_length?: number | null;
native_context_length?: number | null;