Review findings (PR #7473): - Look up overrides under the concrete load path with its quant, not just the advertised repo id, so local folders and non-active HF caches are found. - Carry bare-repo launch flags into the first per-quant save. Auto-switch prefers the qualified entry, so without this the flags were silently dropped and no UI could show or restore them. The bare id is only derived when the suffix looks like a quant, so a Windows drive letter is not split. - Drop a saved gpu_ids pin that no longer resolves instead of 400ing the whole load. A pin outlives the machine it was made on. - Build the displayed API base from getApiBase() on desktop; the Tauri webview origin is not the API server. - Keep a partial download's isDownloaded when opening settings, so the loader still reports download progress. - Only prefer the loaded quant when the loaded model is this row. Q4_K_M exists in most repos, so an unguarded match targeted the wrong variant. Found by simulation: - A lone surrogate in a chat template raised UnicodeEncodeError on the byte check, an unhandled 500. Now a validation error, in all three call sites. - _bounded_int accepted bools as GPU ids, truncated fractional floats, and raised OverflowError on Infinity, which json.loads accepts. - api_monitor stored a non-string model verbatim; the monitor page then threw on toLowerCase and rendered nothing. Coerced at the boundary and the filter no longer trusts network data. - The overlay store now uses storage that cannot throw. Safari private mode and blocked-cookie origins make localStorage throw on access, which broke the opt-out toggle.
48 lines
1.6 KiB
Python
48 lines
1.6 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
# Mirror the frontend's 64 KiB chat-template contract (per-model-config.ts) at
|
|
# the API boundary so a direct caller cannot make Jinja parse an oversized
|
|
# template. MaxBodyMiddleware only caps the whole request body, not this field.
|
|
MAX_CHAT_TEMPLATE_BYTES = 65_536
|
|
|
|
|
|
def chat_template_byte_length(value: str) -> Optional[int]:
|
|
"""UTF-8 length, or None if the string cannot be encoded at all.
|
|
|
|
JSON can carry an unpaired surrogate, as a truncated emoji paste produces.
|
|
json decodes it fine and .encode("utf-8") then raises. Callers treat None as
|
|
"reject": such a template can never render.
|
|
"""
|
|
try:
|
|
return len(value.encode("utf-8"))
|
|
except UnicodeEncodeError:
|
|
return None
|
|
|
|
|
|
class ValidateChatTemplateRequest(BaseModel):
|
|
template: str = Field(default = "")
|
|
|
|
@field_validator("template")
|
|
@classmethod
|
|
def _enforce_template_size(cls, value: str) -> str:
|
|
size = chat_template_byte_length(value)
|
|
if size is None:
|
|
raise ValueError("Chat template contains unpaired surrogate characters.")
|
|
if size > MAX_CHAT_TEMPLATE_BYTES:
|
|
raise ValueError(f"Chat template exceeds the {MAX_CHAT_TEMPLATE_BYTES}-byte limit.")
|
|
return value
|
|
|
|
|
|
class ValidateChatTemplateResponse(BaseModel):
|
|
valid: bool
|
|
error: Optional[str] = None
|
|
|
|
|
|
class ModelTemplateResponse(BaseModel):
|
|
model_name: str
|
|
chat_template: Optional[str] = None
|