diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index c1a37e2fa4..ae8224d424 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -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", diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index eb2bb9c5ce..027f3313a1 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -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" ) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index b13eb08967..53dd851666 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -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: diff --git a/studio/backend/tests/test_native_context_length.py b/studio/backend/tests/test_native_context_length.py index 7c69e56f89..60622c776d 100644 --- a/studio/backend/tests/test_native_context_length.py +++ b/studio/backend/tests/test_native_context_length.py @@ -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( 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 cfac6dc8cf..d9e817678b 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 @@ -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, }); diff --git a/studio/frontend/src/features/chat/types/api.ts b/studio/frontend/src/features/chat/types/api.ts index 25957f4a7b..15d0457722 100644 --- a/studio/frontend/src/features/chat/types/api.ts +++ b/studio/frontend/src/features/chat/types/api.ts @@ -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;