Add per-model generation settings to the model selector

Each text and vision harness setting now carries the generation settings
its model takes (temperature, sampling limits, penalties, seed, reasoning
effort), edited in a modal under the model pick so the harness block stays
short. Nothing is set by default, so models keep their provider's own
values until a super admin or admin changes them.

A setting is only offered and only sent when the catalog says the model
takes it: OpenRouter's supported_parameters is now stored on
harness_models and refreshed by the hourly sync. Other providers list no
parameters, so their models get them all.
This commit is contained in:
2026-10-22 04:52:55 +01:00
parent fe502b38fe
commit c2553a4746
25 changed files with 644 additions and 13 deletions
@@ -0,0 +1,78 @@
import Input from "@moduletrace/twui/form/Input";
import Select from "@moduletrace/twui/form/Select";
import type { HarnessParameter } from "@/src/dict/harness-parameter-dict";
import type { HarnessParameterValue } from "@/src/dict/harness-parameter-dict";
export const DefaultParameterValue = "default";
type Props = {
id: string;
parameter: HarnessParameter;
value?: HarnessParameterValue;
onChange: (value: HarnessParameterValue | undefined) => void;
};
/**
* One generation setting: a number field (empty for the provider's
* default) or a pick from fixed choices ("Default" for the provider's).
*/
export default function HarnessParameterControl({
id,
parameter,
value,
onChange,
}: Props) {
if (parameter.value_type == "select") {
return (
<Select
id={id}
title={parameter.title}
label={parameter.title}
showLabel
info={parameter.description}
value={value ? String(value) : DefaultParameterValue}
options={[
{
value: DefaultParameterValue,
title: "Default (the model's own)",
},
...parameter.options.map((option) => ({
value: option.value,
title: option.title,
})),
]}
changeHandler={(picked) =>
onChange(
picked == DefaultParameterValue ? undefined : picked,
)
}
/>
);
}
return (
<Input
id={id}
title={parameter.title}
label={parameter.title}
showLabel
info={parameter.description}
numberText
min={parameter.min}
max={"max" in parameter ? parameter.max : undefined}
step={parameter.step}
defaultValue={value === undefined ? "" : String(value)}
placeholder="Default"
changeHandler={(next) => {
if (next.trim() == "") {
onChange(undefined);
return;
}
const number = Number(next);
if (!Number.isNaN(number)) onChange(number);
}}
/>
);
}
@@ -0,0 +1,58 @@
import {
HarnessParameters,
modelTakesParameter,
} from "@/src/dict/harness-parameter-dict";
import type { HarnessModelOption } from "@/src/types";
import HarnessParameterControl from "./harness-parameter-control";
type Props = {
id: string;
/**
* The model whose settings these are: only what it takes is shown.
*/
model?: HarnessModelOption;
params?: Record<string, number | string>;
onChange: (params: Record<string, number | string>) => void;
};
/**
* The generation settings the picked model takes: temperature, sampling
* limits, penalties, seed and reasoning effort. A model whose provider
* lists no parameters (anything but OpenRouter) gets them all.
*/
export default function HarnessParameterFields({
id,
model,
params,
onChange,
}: Props) {
const shown = HarnessParameters.filter((parameter) =>
model
? modelTakesParameter({
parameter,
supported: model.supported_parameters,
})
: false,
);
return (
<>
{shown.map((parameter) => (
<HarnessParameterControl
key={parameter.key}
id={`${id}-${parameter.key}`}
parameter={parameter}
value={params?.[parameter.key]}
onChange={(value) => {
const next = { ...params };
if (value === undefined) delete next[parameter.key];
else next[parameter.key] = value;
onChange(next);
}}
/>
))}
</>
);
}
@@ -0,0 +1,88 @@
import { useState } from "react";
import Modal from "@moduletrace/twui/elements/Modal";
import H2 from "@moduletrace/twui/layout/H2";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import { HarnessParameters } from "@/src/dict/harness-parameter-dict";
import type { HarnessModelOption } from "@/src/types";
import HarnessParameterFields from "./harness-parameter-fields";
type Props = {
id: string;
/**
* The model the settings are for: only what it takes is offered.
*/
model: HarnessModelOption;
params?: Record<string, number | string>;
onChange: (params: Record<string, number | string>) => void;
};
/**
* The generation settings for the picked model (temperature, reasoning
* effort, sampling limits), in a modal so they don't lengthen the harness
* block. The target reads "Model settings" and names what is set.
*/
export default function HarnessParameterModal({
id,
model,
params,
onChange,
}: Props) {
const [open, setOpen] = useState(false);
const set = HarnessParameters.flatMap((parameter) => {
const value = params?.[parameter.key];
if (value === undefined || value === "") return [];
const label =
parameter.value_type == "select"
? parameter.options.find((option) => option.value == value)
?.title || String(value)
: String(value);
return [`${parameter.title} ${label}`];
});
return (
<Modal
target={
<Stack className="gap-0.5! items-start cursor-pointer">
<Span className="text-sm text-ink underline decoration-hairline-strong underline-offset-2">
Model settings
</Span>
<Span className="text-xs leading-4 text-ink-secondary">
{set.length
? set.join(" · ")
: "Temperature, reasoning effort, sampling limits"}
</Span>
</Stack>
}
targetWrapperProps={{
className:
"self-start rounded-default-sm focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2 focus-visible:ring-offset-paper",
}}
open={open}
setOpen={setOpen}
className="max-w-lg bg-paper p-6"
>
<Stack className="w-full gap-1! items-stretch pb-2">
<H2 className="text-lg! leading-6! tracking-normal! font-bold text-ink">
{model.name} settings
</H2>
<Span className="text-[13px] leading-[18px] text-ink-secondary">
How this model answers. Empty leaves the provider's own
default; a lower temperature keeps a bot literal and
repeatable.
</Span>
</Stack>
<Stack className="w-full gap-4! items-stretch">
<HarnessParameterFields
id={id}
model={model}
params={params}
onChange={onChange}
/>
</Stack>
</Modal>
);
}
@@ -1,8 +1,10 @@
import Checkbox from "@moduletrace/twui/form/Checkbox";
import { HarnessParameterHarnesses } from "@/src/dict/harness-parameter-dict";
import type { HarnessFormValue, HarnessOverview } from "@/src/types";
import SettingsGroup from "../../(partials)/settings-group";
import HarnessCurrentModel from "./harness-current-model";
import HarnessModelSelect from "./harness-model-select";
import HarnessParameterModal from "./harness-parameter-modal";
import HarnessProviderSelect from "./harness-provider-select";
import HarnessVoicePreview from "./harness-voice-preview";
import HarnessVoiceSelect from "./harness-voice-select";
@@ -28,6 +30,15 @@ export default function HarnessSettingGroup({ harness, value, onChange }: Props)
: harness.models;
const picked = models.find((model) => model.id == value.model_id);
const voices = (picked || harness.current)?.voices;
/**
* The model the settings are for: the pick, else the one it runs on now.
*/
const params_model = picked || harness.current;
const shows_params =
!value.inherit &&
HarnessParameterHarnesses.includes(
harness.harness as (typeof HarnessParameterHarnesses)[number],
);
const on_bot_page = harness.default !== undefined;
const default_name = harness.default?.enabled === false
? "turned off"
@@ -79,6 +90,14 @@ export default function HarnessSettingGroup({ harness, value, onChange }: Props)
changeHandler={(fallback) => onChange({ ...value, fallback })}
/>
) : null}
{shows_params && params_model ? (
<HarnessParameterModal
id={id}
model={params_model}
params={value.params}
onChange={(params) => onChange({ ...value, params })}
/>
) : null}
{!value.inherit && harness.harness == "speech" && voices?.length ? (
<HarnessVoiceSelect
id={`${id}-voice`}
@@ -30,6 +30,22 @@ function isSame({ a, b }: { a: HarnessFormValue; b?: HarnessFormValue }) {
(a.provider_id ?? null) == (b!.provider_id ?? null) &&
a.fallback == b!.fallback &&
a.enabled == b!.enabled &&
(a.voice || "") == (b!.voice || "")
(a.voice || "") == (b!.voice || "") &&
paramsKey({ params: a.params }) == paramsKey({ params: b!.params })
);
}
/**
* The generation settings as one comparable string.
*/
export function paramsKey({
params,
}: {
params?: Record<string, number | string>;
}) {
const entries = Object.entries(params || {}).sort(([a], [b]) =>
a.localeCompare(b),
);
return JSON.stringify(entries);
}
@@ -1,3 +1,4 @@
import { harnessParametersFromOptions } from "@/src/dict/harness-parameter-dict";
import type {
HarnessOverview,
HarnessSettingsFormObject,
@@ -26,6 +27,9 @@ export default function toHarnessForm({
typeof harness.options.voice == "string"
? harness.options.voice
: undefined,
params: harnessParametersFromOptions({
options: harness.options,
}),
},
]),
);