mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 14:23:58 +00:00
feat(webui): add context window setting
This commit is contained in:
parent
3a420136bb
commit
404b68cdd4
@ -85,6 +85,7 @@ _IMAGE_GENERATION_ASPECT_RATIOS = {
|
||||
"2:3",
|
||||
"21:9",
|
||||
}
|
||||
_CONTEXT_WINDOW_TOKEN_OPTIONS = {65_536, 262_144}
|
||||
_MODEL_CONFIGURATION_SLUG_RE = re.compile(r"[^a-z0-9_-]+")
|
||||
|
||||
|
||||
@ -257,6 +258,18 @@ def _parse_bool(value: str, field: str) -> bool:
|
||||
return normalized in {"1", "true", "yes"}
|
||||
|
||||
|
||||
def _parse_context_window_tokens(value: str | None) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
parsed = int(value)
|
||||
except ValueError:
|
||||
raise WebUISettingsError("context_window_tokens must be an integer") from None
|
||||
if parsed not in _CONTEXT_WINDOW_TOKEN_OPTIONS:
|
||||
raise WebUISettingsError("context_window_tokens must be 65536 or 262144")
|
||||
return parsed
|
||||
|
||||
|
||||
def _model_configuration_slug(label: str) -> str:
|
||||
normalized = _MODEL_CONFIGURATION_SLUG_RE.sub("-", label.strip().lower())
|
||||
normalized = normalized.strip("-_")
|
||||
@ -544,6 +557,16 @@ def update_agent_settings(query: QueryParams) -> dict[str, Any]:
|
||||
defaults.provider = provider
|
||||
changed = True
|
||||
|
||||
context_window_tokens = _parse_context_window_tokens(
|
||||
_query_first_alias(query, "context_window_tokens", "contextWindowTokens")
|
||||
)
|
||||
if (
|
||||
context_window_tokens is not None
|
||||
and defaults.context_window_tokens != context_window_tokens
|
||||
):
|
||||
defaults.context_window_tokens = context_window_tokens
|
||||
changed = True
|
||||
|
||||
timezone = _query_first(query, "timezone")
|
||||
if timezone is not None:
|
||||
timezone = timezone.strip()
|
||||
@ -671,6 +694,16 @@ def update_model_configuration(query: QueryParams) -> dict[str, Any]:
|
||||
preset.provider = provider
|
||||
changed = True
|
||||
|
||||
context_window_tokens = _parse_context_window_tokens(
|
||||
_query_first_alias(query, "context_window_tokens", "contextWindowTokens")
|
||||
)
|
||||
if (
|
||||
context_window_tokens is not None
|
||||
and preset.context_window_tokens != context_window_tokens
|
||||
):
|
||||
preset.context_window_tokens = context_window_tokens
|
||||
changed = True
|
||||
|
||||
if config.agents.defaults.model_preset != name:
|
||||
config.agents.defaults.model_preset = name
|
||||
changed = True
|
||||
|
||||
@ -4,7 +4,10 @@ from unittest.mock import MagicMock
|
||||
|
||||
from nanobot.agent.loop import AgentLoop
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.providers.factory import ProviderSnapshot
|
||||
from nanobot.config.loader import save_config
|
||||
from nanobot.config.schema import Config
|
||||
from nanobot.providers.factory import ProviderSnapshot, load_provider_snapshot
|
||||
from nanobot.webui.settings_api import update_agent_settings
|
||||
|
||||
|
||||
def _provider(default_model: str, max_tokens: int = 123) -> MagicMock:
|
||||
@ -72,3 +75,30 @@ def test_llm_runtime_refreshes_provider_snapshot(tmp_path: Path) -> None:
|
||||
assert runtime.model == "new-model"
|
||||
assert loop.provider is new_provider
|
||||
assert loop.runner.provider is new_provider
|
||||
|
||||
|
||||
def test_settings_context_window_refreshes_runtime_state(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config = Config()
|
||||
config.agents.defaults.workspace = str(tmp_path / "workspace")
|
||||
config.agents.defaults.model = "openai/gpt-4o"
|
||||
config.agents.defaults.provider = "openai"
|
||||
config.agents.defaults.context_window_tokens = 65_536
|
||||
config.providers.openai.api_key = "sk-test"
|
||||
save_config(config, config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
def loader(*, preset_name: str | None = None) -> ProviderSnapshot:
|
||||
return load_provider_snapshot(config_path, preset_name=preset_name)
|
||||
|
||||
loop = AgentLoop.from_config(config, provider_snapshot_loader=loader)
|
||||
|
||||
payload = update_agent_settings({"context_window_tokens": ["262144"]})
|
||||
loop._refresh_provider_snapshot()
|
||||
|
||||
assert payload["requires_restart"] is False
|
||||
assert loop.context_window_tokens == 262_144
|
||||
assert loop.consolidator.context_window_tokens == 262_144
|
||||
|
||||
@ -11,6 +11,7 @@ from nanobot.webui.settings_api import (
|
||||
_oauth_provider_status,
|
||||
create_model_configuration,
|
||||
settings_payload,
|
||||
update_agent_settings,
|
||||
update_model_configuration,
|
||||
update_network_safety_settings,
|
||||
)
|
||||
@ -119,6 +120,60 @@ def test_update_model_configuration_edits_named_preset_and_selects(
|
||||
assert saved.model_presets["codex"].model == "openai-codex/gpt-5.5"
|
||||
|
||||
|
||||
def test_update_agent_settings_accepts_context_window_options(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config = Config()
|
||||
save_config(config, config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
payload = update_agent_settings({"context_window_tokens": ["262144"]})
|
||||
|
||||
assert payload["agent"]["context_window_tokens"] == 262144
|
||||
saved = load_config(config_path)
|
||||
assert saved.agents.defaults.context_window_tokens == 262144
|
||||
|
||||
|
||||
def test_update_model_configuration_accepts_context_window_options(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config = Config()
|
||||
config.model_presets["codex"] = ModelPresetConfig(
|
||||
label="Codex",
|
||||
provider="openai",
|
||||
model="openai/gpt-4.1",
|
||||
)
|
||||
save_config(config, config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
payload = update_model_configuration(
|
||||
{
|
||||
"name": ["codex"],
|
||||
"context_window_tokens": ["262144"],
|
||||
}
|
||||
)
|
||||
|
||||
assert payload["agent"]["context_window_tokens"] == 262144
|
||||
saved = load_config(config_path)
|
||||
assert saved.model_presets["codex"].context_window_tokens == 262144
|
||||
|
||||
|
||||
def test_update_context_window_rejects_unknown_values(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(Config(), config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
with pytest.raises(WebUISettingsError, match="context_window_tokens must be 65536 or 262144"):
|
||||
update_agent_settings({"context_window_tokens": ["128000"]})
|
||||
|
||||
|
||||
def test_update_model_configuration_rejects_default_preset(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
@ -139,6 +139,7 @@ interface AgentSettingsDraft {
|
||||
provider: string;
|
||||
modelPreset: string;
|
||||
presetLabel: string;
|
||||
contextWindowTokens: number;
|
||||
timezone: string;
|
||||
botName: string;
|
||||
botIcon: string;
|
||||
@ -164,6 +165,7 @@ type ProviderForm = { apiKey: string; apiBase: string; apiType: ProviderApiType
|
||||
type CustomMcpTransport = "stdio" | "streamableHttp" | "sse";
|
||||
|
||||
const NANOBOT_ICON_SRC = "/brand/nanobot_icon.png";
|
||||
const CONTEXT_WINDOW_TOKEN_OPTIONS = [65_536, 262_144] as const;
|
||||
|
||||
const FALLBACK_TIMEZONES = [
|
||||
"UTC",
|
||||
@ -279,6 +281,10 @@ function defaultPreset(payload: SettingsPayload): SettingsPayload["model_presets
|
||||
return payload.model_presets.find((preset) => preset.is_default) ?? null;
|
||||
}
|
||||
|
||||
function normalizeContextWindowTokens(value: number | null | undefined): number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 65_536;
|
||||
}
|
||||
|
||||
function editableDefaultProvider(payload: SettingsPayload): string {
|
||||
const base = defaultPreset(payload);
|
||||
return base?.provider ?? payload.agent.provider ?? payload.agent.resolved_provider ?? "";
|
||||
@ -373,6 +379,7 @@ export function SettingsView({
|
||||
provider: "",
|
||||
modelPreset: "default",
|
||||
presetLabel: "Default",
|
||||
contextWindowTokens: 65_536,
|
||||
timezone: "UTC",
|
||||
botName: "nanobot",
|
||||
botIcon: "",
|
||||
@ -398,6 +405,9 @@ export function SettingsView({
|
||||
: activePreset?.provider ?? editableDefaultProvider(payload),
|
||||
modelPreset: activePresetName,
|
||||
presetLabel: activePreset?.label ?? activePresetName,
|
||||
contextWindowTokens: normalizeContextWindowTokens(
|
||||
activePreset?.context_window_tokens ?? payload.agent.context_window_tokens,
|
||||
),
|
||||
timezone: payload.agent.timezone,
|
||||
botName: payload.agent.bot_name,
|
||||
botIcon: payload.agent.bot_icon,
|
||||
@ -533,6 +543,7 @@ export function SettingsView({
|
||||
form.modelPreset !== activePresetName ||
|
||||
form.model !== selectedPreset.model ||
|
||||
form.provider !== selectedProvider ||
|
||||
form.contextWindowTokens !== normalizeContextWindowTokens(selectedPreset.context_window_tokens) ||
|
||||
(!selectedPreset.is_default && form.presetLabel.trim() !== selectedPreset.label)
|
||||
);
|
||||
}, [form, settings]);
|
||||
@ -644,14 +655,23 @@ export function SettingsView({
|
||||
label: form.presetLabel.trim(),
|
||||
model: form.model,
|
||||
provider: form.provider,
|
||||
...(form.contextWindowTokens !== selectedPreset.context_window_tokens
|
||||
? { contextWindowTokens: form.contextWindowTokens }
|
||||
: {}),
|
||||
});
|
||||
} else {
|
||||
const defaultModel = defaultPreset(settings)?.model ?? settings.agent.model;
|
||||
const defaultProvider = editableDefaultProvider(settings);
|
||||
const defaultContextWindowTokens = normalizeContextWindowTokens(
|
||||
defaultPreset(settings)?.context_window_tokens ?? settings.agent.context_window_tokens,
|
||||
);
|
||||
payload = await updateSettings(token, {
|
||||
modelPreset: form.modelPreset,
|
||||
...(form.model !== defaultModel ? { model: form.model } : {}),
|
||||
...(form.provider !== defaultProvider ? { provider: form.provider } : {}),
|
||||
...(form.contextWindowTokens !== defaultContextWindowTokens
|
||||
? { contextWindowTokens: form.contextWindowTokens }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
applyPayload(payload);
|
||||
@ -1876,6 +1896,9 @@ function ModelsSettings({
|
||||
? editableDefaultProvider(settings)
|
||||
: nextPreset?.provider ?? prev.provider,
|
||||
presetLabel: nextPreset?.label ?? modelPreset,
|
||||
contextWindowTokens: normalizeContextWindowTokens(
|
||||
nextPreset?.context_window_tokens ?? prev.contextWindowTokens,
|
||||
),
|
||||
}));
|
||||
}}
|
||||
onCreateConfiguration={onCreateConfiguration}
|
||||
@ -1895,49 +1918,73 @@ function ModelsSettings({
|
||||
/>
|
||||
</SettingsRow>
|
||||
) : null}
|
||||
<SettingsRow
|
||||
title={t("settings.rows.provider")}
|
||||
description={t("settings.help.provider")}
|
||||
<SettingsRow
|
||||
title={t("settings.rows.provider")}
|
||||
description={t("settings.help.provider")}
|
||||
>
|
||||
<ProviderPicker
|
||||
providers={providerOptions}
|
||||
value={providerValue}
|
||||
emptyLabel={t("settings.byok.noConfiguredProviders")}
|
||||
showProviderLogos={showBrandLogos}
|
||||
onChange={(provider) => setForm((prev) => ({ ...prev, provider }))}
|
||||
/>
|
||||
</SettingsRow>
|
||||
{selectedProviderNeedsSignIn ? (
|
||||
<SettingsRow
|
||||
title={tx("settings.oauth.signInRequired", "Sign in required")}
|
||||
description={tx(
|
||||
"settings.oauth.signInBeforeSaving",
|
||||
"Sign in before saving this OAuth provider as the active model provider.",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => selectedProvider && onProviderOAuthLogin(selectedProvider.name)}
|
||||
disabled={!selectedProvider?.oauth_login_supported || selectedProviderSigningIn}
|
||||
className="rounded-full"
|
||||
>
|
||||
<ProviderPicker
|
||||
providers={providerOptions}
|
||||
value={providerValue}
|
||||
emptyLabel={t("settings.byok.noConfiguredProviders")}
|
||||
showProviderLogos={showBrandLogos}
|
||||
onChange={(provider) => setForm((prev) => ({ ...prev, provider }))}
|
||||
/>
|
||||
</SettingsRow>
|
||||
{selectedProviderNeedsSignIn ? (
|
||||
<SettingsRow
|
||||
title={tx("settings.oauth.signInRequired", "Sign in required")}
|
||||
description={tx("settings.oauth.signInBeforeSaving", "Sign in before saving this OAuth provider as the active model provider.")}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => selectedProvider && onProviderOAuthLogin(selectedProvider.name)}
|
||||
disabled={!selectedProvider?.oauth_login_supported || selectedProviderSigningIn}
|
||||
className="rounded-full"
|
||||
>
|
||||
{selectedProviderSigningIn ? (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||
) : null}
|
||||
{selectedProviderSigningIn
|
||||
? tx("settings.oauth.signingIn", "Signing in...")
|
||||
: tx("settings.oauth.signIn", "Sign in")}
|
||||
</Button>
|
||||
</SettingsRow>
|
||||
) : null}
|
||||
<SettingsRow
|
||||
title={t("settings.rows.model")}
|
||||
description={t("settings.help.model")}
|
||||
>
|
||||
<Input
|
||||
value={form.model}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, model: event.target.value }))}
|
||||
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
|
||||
/>
|
||||
</SettingsRow>
|
||||
{selectedProviderSigningIn ? (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||
) : null}
|
||||
{selectedProviderSigningIn
|
||||
? tx("settings.oauth.signingIn", "Signing in...")
|
||||
: tx("settings.oauth.signIn", "Sign in")}
|
||||
</Button>
|
||||
</SettingsRow>
|
||||
) : null}
|
||||
<SettingsRow
|
||||
title={t("settings.rows.model")}
|
||||
description={t("settings.help.model")}
|
||||
>
|
||||
<Input
|
||||
value={form.model}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, model: event.target.value }))}
|
||||
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={tx("settings.rows.contextWindow", "Context window")}
|
||||
description={tx(
|
||||
"settings.help.contextWindow",
|
||||
"Choose the default context budget for this model configuration.",
|
||||
)}
|
||||
>
|
||||
<SegmentedControl
|
||||
value={String(form.contextWindowTokens)}
|
||||
options={CONTEXT_WINDOW_TOKEN_OPTIONS.map((tokens) => ({
|
||||
value: String(tokens),
|
||||
label: tokens === 262_144 ? "256K" : "64K",
|
||||
}))}
|
||||
onChange={(value) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
contextWindowTokens: normalizeContextWindowTokens(Number(value)),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsFooter
|
||||
dirty={dirty}
|
||||
saving={saving}
|
||||
|
||||
@ -146,7 +146,8 @@
|
||||
"cliAppsFilter": "Filter",
|
||||
"engine": "Engine",
|
||||
"logs": "Logs",
|
||||
"diagnostics": "Diagnostics"
|
||||
"diagnostics": "Diagnostics",
|
||||
"contextWindow": "Context window"
|
||||
},
|
||||
"help": {
|
||||
"theme": "Switch between light and dark appearance.",
|
||||
@ -184,7 +185,8 @@
|
||||
"logs": "Open the native engine log folder.",
|
||||
"diagnostics": "Export a small runtime report for support.",
|
||||
"localServiceAccessNative": "Allow Full Access shell commands to reach services on this Mac.",
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission."
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission.",
|
||||
"contextWindow": "Choose the default context budget for this model configuration."
|
||||
},
|
||||
"timezone": {
|
||||
"select": "Select timezone",
|
||||
|
||||
@ -137,7 +137,8 @@
|
||||
"cliAppsFilter": "Filtro de apps CLI",
|
||||
"engine": "Motor",
|
||||
"logs": "Registros",
|
||||
"diagnostics": "Diagnóstico"
|
||||
"diagnostics": "Diagnóstico",
|
||||
"contextWindow": "Context window"
|
||||
},
|
||||
"help": {
|
||||
"theme": "Cambia entre apariencia clara y oscura.",
|
||||
@ -175,7 +176,8 @@
|
||||
"logs": "Abre la carpeta de registros del motor de escritorio.",
|
||||
"diagnostics": "Exporta un pequeño informe de runtime para soporte.",
|
||||
"localServiceAccessNative": "Allow Full Access shell commands to reach services on this Mac.",
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission."
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission.",
|
||||
"contextWindow": "Choose the default context budget for this model configuration."
|
||||
},
|
||||
"values": {
|
||||
"light": "Claro",
|
||||
|
||||
@ -137,7 +137,8 @@
|
||||
"cliAppsFilter": "Filtre des apps CLI",
|
||||
"engine": "Moteur",
|
||||
"logs": "Journaux",
|
||||
"diagnostics": "Diagnostics"
|
||||
"diagnostics": "Diagnostics",
|
||||
"contextWindow": "Context window"
|
||||
},
|
||||
"help": {
|
||||
"theme": "Basculer entre les apparences claire et sombre.",
|
||||
@ -175,7 +176,8 @@
|
||||
"logs": "Ouvrir le dossier des journaux du moteur natif.",
|
||||
"diagnostics": "Exporter un petit rapport runtime pour le support.",
|
||||
"localServiceAccessNative": "Allow Full Access shell commands to reach services on this Mac.",
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission."
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission.",
|
||||
"contextWindow": "Choose the default context budget for this model configuration."
|
||||
},
|
||||
"values": {
|
||||
"light": "Clair",
|
||||
|
||||
@ -137,7 +137,8 @@
|
||||
"cliAppsFilter": "Filter aplikasi CLI",
|
||||
"engine": "Engine",
|
||||
"logs": "Log",
|
||||
"diagnostics": "Diagnostik"
|
||||
"diagnostics": "Diagnostik",
|
||||
"contextWindow": "Context window"
|
||||
},
|
||||
"help": {
|
||||
"theme": "Beralih antara tampilan terang dan gelap.",
|
||||
@ -175,7 +176,8 @@
|
||||
"logs": "Buka folder log native engine.",
|
||||
"diagnostics": "Ekspor laporan runtime kecil untuk dukungan.",
|
||||
"localServiceAccessNative": "Allow Full Access shell commands to reach services on this Mac.",
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission."
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission.",
|
||||
"contextWindow": "Choose the default context budget for this model configuration."
|
||||
},
|
||||
"values": {
|
||||
"light": "Terang",
|
||||
|
||||
@ -137,7 +137,8 @@
|
||||
"cliAppsFilter": "CLI アプリフィルター",
|
||||
"engine": "エンジン",
|
||||
"logs": "ログ",
|
||||
"diagnostics": "診断"
|
||||
"diagnostics": "診断",
|
||||
"contextWindow": "Context window"
|
||||
},
|
||||
"help": {
|
||||
"theme": "ライト表示とダーク表示を切り替えます。",
|
||||
@ -175,7 +176,8 @@
|
||||
"logs": "Appエンジンのログフォルダを開きます。",
|
||||
"diagnostics": "サポート用の小さなランタイムレポートを書き出します。",
|
||||
"localServiceAccessNative": "Allow Full Access shell commands to reach services on this Mac.",
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission."
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission.",
|
||||
"contextWindow": "Choose the default context budget for this model configuration."
|
||||
},
|
||||
"values": {
|
||||
"light": "ライト",
|
||||
|
||||
@ -137,7 +137,8 @@
|
||||
"cliAppsFilter": "CLI 앱 필터",
|
||||
"engine": "엔진",
|
||||
"logs": "로그",
|
||||
"diagnostics": "진단"
|
||||
"diagnostics": "진단",
|
||||
"contextWindow": "Context window"
|
||||
},
|
||||
"help": {
|
||||
"theme": "밝은 모드와 어두운 모드를 전환합니다.",
|
||||
@ -175,7 +176,8 @@
|
||||
"logs": "App 엔진 로그 폴더를 엽니다.",
|
||||
"diagnostics": "지원용 작은 런타임 보고서를 내보냅니다.",
|
||||
"localServiceAccessNative": "Allow Full Access shell commands to reach services on this Mac.",
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission."
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission.",
|
||||
"contextWindow": "Choose the default context budget for this model configuration."
|
||||
},
|
||||
"values": {
|
||||
"light": "라이트",
|
||||
|
||||
@ -137,7 +137,8 @@
|
||||
"cliAppsFilter": "Bộ lọc ứng dụng CLI",
|
||||
"engine": "Engine",
|
||||
"logs": "Nhật ký",
|
||||
"diagnostics": "Chẩn đoán"
|
||||
"diagnostics": "Chẩn đoán",
|
||||
"contextWindow": "Context window"
|
||||
},
|
||||
"help": {
|
||||
"theme": "Chuyển giữa giao diện sáng và tối.",
|
||||
@ -175,7 +176,8 @@
|
||||
"logs": "Mở thư mục nhật ký native engine.",
|
||||
"diagnostics": "Xuất báo cáo runtime nhỏ để hỗ trợ.",
|
||||
"localServiceAccessNative": "Allow Full Access shell commands to reach services on this Mac.",
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission."
|
||||
"webuiDefaultAccessNative": "Used by native chats without a project-specific permission.",
|
||||
"contextWindow": "Choose the default context budget for this model configuration."
|
||||
},
|
||||
"values": {
|
||||
"light": "Sáng",
|
||||
|
||||
@ -146,7 +146,8 @@
|
||||
"cliAppsFilter": "筛选",
|
||||
"engine": "引擎",
|
||||
"logs": "日志",
|
||||
"diagnostics": "诊断"
|
||||
"diagnostics": "诊断",
|
||||
"contextWindow": "上下文窗口"
|
||||
},
|
||||
"help": {
|
||||
"theme": "在浅色和深色外观之间切换。",
|
||||
@ -184,7 +185,8 @@
|
||||
"logs": "打开App引擎日志文件夹。",
|
||||
"diagnostics": "导出一份用于支持排查的运行时报告。",
|
||||
"localServiceAccessNative": "允许完全访问模式下的 shell 命令访问这台 Mac 上的服务。",
|
||||
"webuiDefaultAccessNative": "用于没有单独选择权限的原生 App 对话。"
|
||||
"webuiDefaultAccessNative": "用于没有单独选择权限的原生 App 对话。",
|
||||
"contextWindow": "选择这个模型配置默认使用的上下文预算。"
|
||||
},
|
||||
"timezone": {
|
||||
"select": "选择时区",
|
||||
|
||||
@ -137,7 +137,8 @@
|
||||
"cliAppsFilter": "CLI 應用篩選",
|
||||
"engine": "引擎",
|
||||
"logs": "日誌",
|
||||
"diagnostics": "診斷"
|
||||
"diagnostics": "診斷",
|
||||
"contextWindow": "上下文視窗"
|
||||
},
|
||||
"help": {
|
||||
"theme": "在淺色與深色外觀之間切換。",
|
||||
@ -175,7 +176,8 @@
|
||||
"logs": "開啟App引擎日誌資料夾。",
|
||||
"diagnostics": "匯出一份供支援排查用的執行階段報告。",
|
||||
"localServiceAccessNative": "允許完全存取模式下的 shell 命令存取這台 Mac 上的服務。",
|
||||
"webuiDefaultAccessNative": "用於沒有單獨選擇權限的原生 App 對話。"
|
||||
"webuiDefaultAccessNative": "用於沒有單獨選擇權限的原生 App 對話。",
|
||||
"contextWindow": "選擇這個模型設定預設使用的上下文預算。"
|
||||
},
|
||||
"values": {
|
||||
"light": "淺色",
|
||||
|
||||
@ -280,6 +280,9 @@ export async function updateSettings(
|
||||
}
|
||||
if (update.model !== undefined) query.set("model", update.model);
|
||||
if (update.provider !== undefined) query.set("provider", update.provider);
|
||||
if (update.contextWindowTokens !== undefined) {
|
||||
query.set("context_window_tokens", String(update.contextWindowTokens));
|
||||
}
|
||||
if (update.timezone !== undefined) query.set("timezone", update.timezone);
|
||||
if (update.botName !== undefined) query.set("bot_name", update.botName);
|
||||
if (update.botIcon !== undefined) query.set("bot_icon", update.botIcon);
|
||||
@ -315,6 +318,9 @@ export async function updateModelConfiguration(
|
||||
if (configuration.label !== undefined) query.set("label", configuration.label);
|
||||
if (configuration.provider !== undefined) query.set("provider", configuration.provider);
|
||||
if (configuration.model !== undefined) query.set("model", configuration.model);
|
||||
if (configuration.contextWindowTokens !== undefined) {
|
||||
query.set("context_window_tokens", String(configuration.contextWindowTokens));
|
||||
}
|
||||
return request<SettingsPayload>(
|
||||
`${base}/api/settings/model-configurations/update?${query}`,
|
||||
token,
|
||||
|
||||
@ -517,6 +517,7 @@ export interface SettingsUpdate {
|
||||
model?: string;
|
||||
provider?: string;
|
||||
modelPreset?: string | null;
|
||||
contextWindowTokens?: number;
|
||||
timezone?: string;
|
||||
botName?: string;
|
||||
botIcon?: string;
|
||||
@ -535,6 +536,7 @@ export interface ModelConfigurationUpdate {
|
||||
label?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
contextWindowTokens?: number;
|
||||
}
|
||||
|
||||
export interface ProviderSettingsUpdate {
|
||||
|
||||
@ -65,6 +65,7 @@ describe("webui API helpers", () => {
|
||||
modelPreset: "default",
|
||||
model: "openrouter/test",
|
||||
provider: "openrouter",
|
||||
contextWindowTokens: 262144,
|
||||
timezone: "Asia/Shanghai",
|
||||
botName: "nanobot",
|
||||
botIcon: "nb",
|
||||
@ -72,7 +73,7 @@ describe("webui API helpers", () => {
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/settings/update?model_preset=default&model=openrouter%2Ftest&provider=openrouter&timezone=Asia%2FShanghai&bot_name=nanobot&bot_icon=nb&tool_hint_max_length=120",
|
||||
"/api/settings/update?model_preset=default&model=openrouter%2Ftest&provider=openrouter&context_window_tokens=262144&timezone=Asia%2FShanghai&bot_name=nanobot&bot_icon=nb&tool_hint_max_length=120",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
@ -100,10 +101,11 @@ describe("webui API helpers", () => {
|
||||
label: "Codex",
|
||||
provider: "openai_codex",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
contextWindowTokens: 65536,
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/settings/model-configurations/update?name=codex&label=Codex&provider=openai_codex&model=openai-codex%2Fgpt-5.5",
|
||||
"/api/settings/model-configurations/update?name=codex&label=Codex&provider=openai_codex&model=openai-codex%2Fgpt-5.5&context_window_tokens=65536",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
|
||||
@ -121,7 +121,7 @@ const installedAnyGen = {
|
||||
|
||||
function renderSettingsView(
|
||||
options: {
|
||||
initialSection?: "apps" | "advanced";
|
||||
initialSection?: "apps" | "advanced" | "models";
|
||||
onSettingsChange?: (payload: SettingsPayload) => void;
|
||||
} = {},
|
||||
) {
|
||||
@ -222,6 +222,29 @@ describe("SettingsView Apps catalog", () => {
|
||||
await waitFor(() => expect(onSettingsChange).toHaveBeenCalledWith(payload));
|
||||
});
|
||||
|
||||
it("shows context window options in model settings", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/settings") return jsonResponse(settingsPayload());
|
||||
if (url === "/api/settings/cli-apps") {
|
||||
return jsonResponse({ apps: [], installed_count: 0 });
|
||||
}
|
||||
if (url === "/api/settings/mcp-presets") {
|
||||
return jsonResponse({ presets: [], installed_count: 0 });
|
||||
}
|
||||
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
||||
}),
|
||||
);
|
||||
|
||||
renderSettingsView({ initialSection: "models" });
|
||||
|
||||
expect(await screen.findByText("Context window")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "64K" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "256K" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("saves network safety without exposing technical SSRF copy", async () => {
|
||||
const payload = settingsPayload();
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user