1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
|
"""Persistent application settings stored under ``OLLAMA_WEB_DATA_DIR``."""
from __future__ import annotations
import json
import math
import threading
from copy import deepcopy
from pathlib import Path
from typing import Any
NUM_CTX_VALUES = (4096, 8192, 16384, 32768, 65536, 131072, 262144)
INTEGER_OPTIONS = {
"num_ctx",
"num_keep",
"seed",
"num_predict",
"top_k",
"repeat_last_n",
"mirostat",
}
FLOAT_OPTIONS = {
"temperature",
"top_p",
"min_p",
"typical_p",
"tfs_z",
"repeat_penalty",
"presence_penalty",
"frequency_penalty",
"mirostat_tau",
"mirostat_eta",
}
OPTION_KEYS = INTEGER_OPTIONS | FLOAT_OPTIONS | {"stop"}
SUPPORTED_LANGUAGES = {"ja", "en"}
_LOCK = threading.RLock()
class SettingsValidationError(ValueError):
"""Raised when persisted settings do not match the public schema."""
def default_app_settings(language: str = "ja") -> dict[str, Any]:
"""Return a fresh settings object with required slider defaults."""
if language not in SUPPORTED_LANGUAGES:
language = "ja"
return {
"ui": {"language": language},
"ollama": {
"system_prompt": "",
"options": {"temperature": 0.8, "num_ctx": 8192},
},
}
def settings_file(data_dir: str | Path) -> Path:
return Path(data_dir).resolve() / "settings.json"
def _require_keys(value: dict[str, Any], allowed: set[str], where: str) -> None:
unknown = set(value) - allowed
if unknown:
raise SettingsValidationError(f"Unknown {where} field(s): {', '.join(sorted(unknown))}")
def _validate_options(raw: Any) -> dict[str, Any]:
if not isinstance(raw, dict):
raise SettingsValidationError("ollama.options must be an object")
_require_keys(raw, OPTION_KEYS, "option")
cleaned: dict[str, Any] = {}
for key, value in raw.items():
if key in INTEGER_OPTIONS:
if isinstance(value, bool) or not isinstance(value, int):
raise SettingsValidationError(f"ollama.options.{key} must be an integer")
cleaned[key] = value
elif key in FLOAT_OPTIONS:
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise SettingsValidationError(f"ollama.options.{key} must be a number")
number = float(value)
if not math.isfinite(number):
raise SettingsValidationError(f"ollama.options.{key} must be finite")
cleaned[key] = number
elif key == "stop":
if not isinstance(value, list) or not all(
isinstance(item, str) and item for item in value
):
raise SettingsValidationError(
"ollama.options.stop must be an array of non-empty strings"
)
cleaned[key] = list(value)
if "temperature" not in cleaned or not 0 <= cleaned["temperature"] <= 2:
raise SettingsValidationError("ollama.options.temperature must be between 0 and 2")
if cleaned.get("num_ctx") not in NUM_CTX_VALUES:
raise SettingsValidationError("ollama.options.num_ctx is not an allowed context size")
return cleaned
def validate_app_settings(raw: Any) -> dict[str, Any]:
"""Validate and normalize the settings API payload."""
if not isinstance(raw, dict):
raise SettingsValidationError("Settings must be an object")
_require_keys(raw, {"ui", "ollama"}, "settings")
if set(raw) != {"ui", "ollama"}:
raise SettingsValidationError("Settings must contain ui and ollama")
ui = raw["ui"]
if not isinstance(ui, dict):
raise SettingsValidationError("ui must be an object")
_require_keys(ui, {"language"}, "ui")
language = ui.get("language")
if language not in SUPPORTED_LANGUAGES:
raise SettingsValidationError("ui.language must be ja or en")
ollama = raw["ollama"]
if not isinstance(ollama, dict):
raise SettingsValidationError("ollama must be an object")
_require_keys(ollama, {"system_prompt", "options"}, "ollama")
if set(ollama) != {"system_prompt", "options"}:
raise SettingsValidationError("ollama must contain system_prompt and options")
system_prompt = ollama["system_prompt"]
if not isinstance(system_prompt, str):
raise SettingsValidationError("ollama.system_prompt must be a string")
if len(system_prompt) > 20_000:
raise SettingsValidationError("ollama.system_prompt is too long")
return {
"ui": {"language": language},
"ollama": {
"system_prompt": system_prompt,
"options": _validate_options(ollama["options"]),
},
}
def load_app_settings(data_dir: str | Path, default_language: str = "ja") -> dict[str, Any]:
"""Load validated settings, falling back safely when absent or malformed."""
default = default_app_settings(default_language)
path = settings_file(data_dir)
with _LOCK:
if not path.exists():
return default
try:
raw = json.loads(path.read_text(encoding="utf-8"))
return validate_app_settings(raw)
except (OSError, json.JSONDecodeError, SettingsValidationError):
return default
def save_app_settings(data_dir: str | Path, raw: Any) -> dict[str, Any]:
"""Validate and atomically persist settings, returning the normalized value."""
cleaned = validate_app_settings(raw)
path = settings_file(data_dir)
temp_path = path.with_suffix(".json.tmp")
with _LOCK:
path.parent.mkdir(parents=True, exist_ok=True)
temp_path.write_text(
json.dumps(cleaned, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
temp_path.replace(path)
return deepcopy(cleaned)
|