Files
cobble/src/dict/harness-parameter-dict.ts
T
tben 8edff8988a 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.
2026-10-22 04:56:10 +01:00

245 lines
7.3 KiB
TypeScript

/**
* Generation settings a harness can ask its model for. Each is saved in the
* setting's `options` (next to `voice`) and only sent to a model whose
* catalog entry advertises it: a fallback model that rejects a field would
* fail otherwise.
*
* - `key`: the option's key in `options`, and the field sent on the wire
* - `advertised`: the name a provider's model list uses for it, when it
* differs (OpenRouter's `supported_parameters`)
* - `wire`: how the field is written, for providers that nest it
*/
export const HarnessParameters = [
{
key: "temperature",
title: "Temperature",
description: `Lower keeps a bot literal and repeatable, higher makes it more varied. Most models default to 1, which makes small bots invent details.`,
value_type: "number",
min: 0,
max: 2,
step: 0.05,
},
{
key: "top_p",
title: "Top P",
description: `Nucleus sampling: how much of the model's probability mass it may pick from. Change this or temperature, rarely both.`,
value_type: "number",
min: 0,
max: 1,
step: 0.05,
},
{
key: "top_k",
title: "Top K",
description: `How many of the likeliest tokens it may pick from. 0 turns it off. Not every provider takes it.`,
value_type: "number",
min: 0,
max: 200,
step: 1,
},
{
key: "min_p",
title: "Min P",
description: `Drops tokens less likely than this share of the best token. Alternative to top_p.`,
value_type: "number",
min: 0,
max: 1,
step: 0.01,
},
{
key: "top_a",
title: "Top A",
description: `Drops tokens the model is only weakly considering. 0 turns it off.`,
value_type: "number",
min: 0,
max: 1,
step: 0.01,
},
{
key: "frequency_penalty",
title: "Frequency penalty",
description: `Positive values make it repeat words less. Negative values make it repeat more.`,
value_type: "number",
min: -2,
max: 2,
step: 0.1,
},
{
key: "presence_penalty",
title: "Presence penalty",
description: `Positive values push it toward new topics, negative values back to what was already said.`,
value_type: "number",
min: -2,
max: 2,
step: 0.1,
},
{
key: "repetition_penalty",
title: "Repetition penalty",
description: `1 leaves repetition alone, higher penalizes it. Some providers take this instead of the two penalties above.`,
value_type: "number",
min: 0,
max: 2,
step: 0.05,
},
{
key: "seed",
title: "Seed",
description: `Asks for repeatable answers. Providers only do their best: the same seed usually gives the same answer, not always.`,
value_type: "number",
min: 0,
step: 1,
},
{
key: "reasoning_effort",
title: "Reasoning effort",
description: `How long a reasoning model thinks before it answers. "Default" leaves the provider's own setting alone. Models without reasoning ignore it.`,
value_type: "select",
advertised: "reasoning",
wire: "reasoning",
options: [
{ value: "minimal", title: "Minimal" },
{ value: "low", title: "Low" },
{ value: "medium", title: "Medium" },
{ value: "high", title: "High" },
],
},
] as const;
export type HarnessParameter = (typeof HarnessParameters)[number];
export type HarnessParameterKey = HarnessParameter["key"];
export type HarnessParameterValue = number | string;
/**
* The harnesses whose models are chat models, so the only ones the
* generation settings apply to.
*/
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.
*/
export function harnessParametersFromOptions({
options,
}: {
options: Record<string, unknown>;
}): Partial<Record<HarnessParameterKey, HarnessParameterValue>> {
return Object.fromEntries(
HarnessParameters.flatMap((parameter) => {
const value = options[parameter.key];
return typeof value == "number" || (typeof value == "string" && value)
? [[parameter.key, value]]
: [];
}),
);
}
/**
* A `options` object with the given generation settings replaced (and the
* ones missing from it removed), keeping everything else (`voice`).
*/
export function withHarnessParameters({
options,
params,
}: {
options: Record<string, unknown>;
params: Partial<Record<HarnessParameterKey, HarnessParameterValue>>;
}): Record<string, unknown> {
const rest: Record<string, unknown> = Object.fromEntries(
Object.entries(options).filter(([key]) => !HarnessParameterKeys.includes(key as HarnessParameterKey)),
);
return { ...rest, ...params };
}
/**
* Whether a model advertises a setting. A model whose provider lists no
* parameters (anything but OpenRouter) is taken to accept them all.
*/
export function modelTakesParameter({
parameter,
supported,
}: {
parameter: HarnessParameter;
supported?: string[];
}) {
if (!supported?.length) return true;
return supported.includes(
"advertised" in parameter ? parameter.advertised : parameter.key,
);
}