Run models on sensible generation defaults out of the box

A fresh install had no generation settings until someone set one, so every
model ran on whatever its provider happened to use. Each harness now has a
default (text 0.3 temperature, vision 0.2, image the provider's own), shown
in the model settings modal so what a model runs on is never a guess.

The defaults are only sent to a model whose catalog lists the field, so a
provider that says nothing about its models still gets no parameter it
might reject; anything saved on a harness or bot wins over the default.
This commit is contained in:
2026-10-22 04:56:10 +01:00
parent c2553a4746
commit 8edff8988a
6 changed files with 150 additions and 13 deletions
+7
View File
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import {
harnessParameterDefault,
HarnessParameters,
harnessParametersFromOptions,
modelTakesParameter,
@@ -40,6 +41,12 @@ describe("harness parameters", () => {
).toBe(false);
});
test("ships a literal text default and a lookup vision default", () => {
expect(harnessParameterDefault({ harness: "text", key: "temperature" })).toBe(0.3);
expect(harnessParameterDefault({ harness: "vision", key: "temperature" })).toBe(0);
expect(harnessParameterDefault({ harness: "text", key: "seed" })).toBeUndefined();
});
test("a model that lists no parameters takes them all", () => {
expect(
modelTakesParameter({ parameter: temperature, supported: [] }),
+70
View File
@@ -118,6 +118,76 @@ export const HarnessParameterHarnesses = ["text", "vision"] as const;
export const HarnessParameterKeys = HarnessParameters.map((parameter) => parameter.key);
/**
* What each chat harness uses when nobody set anything, so a fresh install
* is ready to go: bots are literal and repeatable (`Cobble`'s job is doing
* the work it was asked for, not improvising), while media reading is a
* lookup, so it stays at 0.
*/
export const HarnessParameterDefaults: Partial<
Record<
(typeof HarnessParameterHarnesses)[number],
Partial<Record<HarnessParameterKey, HarnessParameterValue>>
>
> = {
text: { temperature: 0.3 },
vision: { temperature: 0 },
};
/**
* One setting's value when the harness's own has none.
*/
export function harnessParameterDefault({
harness,
key,
}: {
harness: (typeof HarnessParameterHarnesses)[number];
key: HarnessParameterKey;
}) {
return HarnessParameterDefaults[harness]?.[key];
}
/**
* A setting's value for a harness: what it was saved as, else the app's
* default. `saved` says which, since a default is only used on a model
* that says it takes the field (a saved one is sent to any model whose
* catalog doesn't rule it out).
*/
export function harnessParameterValue({
harness,
params,
key,
}: {
harness: (typeof HarnessParameterHarnesses)[number];
params?: Record<string, unknown>;
key: HarnessParameterKey;
}): { value?: HarnessParameterValue; saved: boolean } {
const saved = params?.[key];
if (typeof saved == "number" || (typeof saved == "string" && saved)) {
return { value: saved, saved: true };
}
return { value: harnessParameterDefault({ harness, key }), saved: false };
}
/**
* Whether a model's provider says it takes a setting (no guessing).
*/
export function modelAdvertisesParameter({
parameter,
supported,
}: {
parameter: HarnessParameter;
supported?: string[];
}) {
return Boolean(
supported?.includes(
"advertised" in parameter ? parameter.advertised : parameter.key,
),
);
}
/**
* The generation settings a saved `options` object holds.
*/
+19 -5
View File
@@ -1,5 +1,7 @@
import {
HarnessParameters,
harnessParameterValue,
modelAdvertisesParameter,
modelTakesParameter,
} from "@/src/dict/harness-parameter-dict";
import grabHarnessSetting from "@/src/functions/backend/db/harness/grab-harness-setting";
@@ -13,10 +15,10 @@ type Params = {
/**
* A model's generation settings for one call: what is saved in the
* harness's setting (the workspace's, the bot's, else the default),
* keyed by the field sent on the wire, keeping only what the model says it
* takes. Empty when none are set, so the model gets its provider's
* defaults.
* harness's setting (the workspace's, the bot's, else the default), else
* `HarnessParameterDefaults` so a fresh install is ready to go. Keyed by
* the field sent on the wire, keeping only what the model says it takes
* (a default only where the model explicitly advertises it).
*/
export default async function grabChatParams({
harness,
@@ -34,7 +36,11 @@ export default async function grabChatParams({
return (model) =>
Object.fromEntries(
HarnessParameters.flatMap((parameter) => {
const value = setting.options[parameter.key];
const { value, saved } = harnessParameterValue({
harness,
params: setting.options,
key: parameter.key,
});
if (typeof value != "number" && typeof value != "string") return [];
if (!value && value !== 0) return [];
@@ -45,6 +51,14 @@ export default async function grabChatParams({
})
)
return [];
if (
!saved &&
!modelAdvertisesParameter({
parameter,
supported: model.supported_parameters,
})
)
return [];
const key = "wire" in parameter ? parameter.wire : parameter.key;
@@ -1,12 +1,20 @@
import {
HarnessParameterHarnesses,
harnessParameterValue,
HarnessParameters,
modelTakesParameter,
type HarnessParameterKey,
} from "@/src/dict/harness-parameter-dict";
import type { HarnessKind } from "@/src/dict/harness-dict";
import type { HarnessModelOption } from "@/src/types";
import HarnessParameterControl from "./harness-parameter-control";
type Props = {
id: string;
/**
* The harness the settings are for: its default fills an empty one.
*/
harness: HarnessKind;
/**
* The model whose settings these are: only what it takes is shown.
*/
@@ -18,10 +26,14 @@ type Props = {
/**
* 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.
* lists no parameters (anything but OpenRouter) gets them all. An empty
* one shows the value it actually runs on (the harness's default). A model
* whose provider lists nothing gets the app's default in its box, but the
* call only sends a default to a model that advertises the field.
*/
export default function HarnessParameterFields({
id,
harness,
model,
params,
onChange,
@@ -42,7 +54,13 @@ export default function HarnessParameterFields({
key={parameter.key}
id={`${id}-${parameter.key}`}
parameter={parameter}
value={params?.[parameter.key]}
value={
harnessParameterValue({
harness: harness as (typeof HarnessParameterHarnesses)[number],
params,
key: parameter.key,
}).value
}
onChange={(value) => {
const next = { ...params };
@@ -3,12 +3,23 @@ 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 {
HarnessParameterHarnesses,
harnessParameterValue,
HarnessParameterKeys,
HarnessParameters,
type HarnessParameterKey,
} from "@/src/dict/harness-parameter-dict";
import type { HarnessKind } from "@/src/dict/harness-dict";
import type { HarnessModelOption } from "@/src/types";
import HarnessParameterFields from "./harness-parameter-fields";
type Props = {
id: string;
/**
* The harness the settings are for: its default fills an empty one.
*/
harness: HarnessKind;
/**
* The model the settings are for: only what it takes is offered.
*/
@@ -24,13 +35,27 @@ type Props = {
*/
export default function HarnessParameterModal({
id,
harness,
model,
params,
onChange,
}: Props) {
const [open, setOpen] = useState(false);
/**
* Nothing saved yet: every value shown is the app's default for it.
*/
const using_defaults = !Object.entries(params || {}).some(
([key, value]) =>
value !== undefined &&
value !== "" &&
HarnessParameterKeys.includes(key as HarnessParameterKey),
);
const set = HarnessParameters.flatMap((parameter) => {
const value = params?.[parameter.key];
const { value } = harnessParameterValue({
harness: harness as (typeof HarnessParameterHarnesses)[number],
params,
key: parameter.key,
});
if (value === undefined || value === "") return [];
@@ -52,7 +77,7 @@ export default function HarnessParameterModal({
</Span>
<Span className="text-xs leading-4 text-ink-secondary">
{set.length
? set.join(" · ")
? `${using_defaults ? "Default: " : ""}${set.join(" · ")}`
: "Temperature, reasoning effort, sampling limits"}
</Span>
</Stack>
@@ -70,14 +95,16 @@ export default function HarnessParameterModal({
{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.
How this model answers. The filled-in values are what it
runs on now (the app's default for this harness, else the
provider's own); a lower temperature keeps a bot literal
and repeatable.
</Span>
</Stack>
<Stack className="w-full gap-4! items-stretch">
<HarnessParameterFields
id={id}
harness={harness}
model={model}
params={params}
onChange={onChange}
@@ -93,6 +93,7 @@ export default function HarnessSettingGroup({ harness, value, onChange }: Props)
{shows_params && params_model ? (
<HarnessParameterModal
id={id}
harness={harness.harness}
model={params_model}
params={value.params}
onChange={(params) => onChange({ ...value, params })}