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:
@@ -3,6 +3,7 @@ import HarnessError from "../harness-error";
|
||||
import grabHarnessAdapter from "../providers/grab-harness-adapter";
|
||||
import resolveHarnessCandidates from "../resolve/resolve-harness-candidates";
|
||||
import withCandidates from "../resolve/with-candidates";
|
||||
import grabChatParams from "../text/grab-chat-params";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
@@ -35,6 +36,12 @@ export default async function readImage({ url, focus, signal, bot_id, workspace_
|
||||
workspace_id,
|
||||
});
|
||||
|
||||
const saved_params = await grabChatParams({
|
||||
harness: "vision",
|
||||
bot_id,
|
||||
workspace_id,
|
||||
});
|
||||
|
||||
const { result } = await withCandidates({
|
||||
harness: "vision",
|
||||
candidates,
|
||||
@@ -57,7 +64,14 @@ export default async function readImage({ url, focus, signal, bot_id, workspace_
|
||||
],
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
/**
|
||||
* Reading is a lookup, so it stays literal unless a
|
||||
* temperature was set for the vision harness.
|
||||
*/
|
||||
params: {
|
||||
temperature: 0,
|
||||
...saved_params(candidate.model),
|
||||
},
|
||||
signal: signal || new AbortController().signal,
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const chat: HarnessAdapter["chat"] = async ({
|
||||
messages,
|
||||
tools,
|
||||
max_tokens,
|
||||
temperature,
|
||||
params,
|
||||
signal,
|
||||
}) => {
|
||||
const { provider, model } = candidate;
|
||||
@@ -32,7 +32,7 @@ const chat: HarnessAdapter["chat"] = async ({
|
||||
}
|
||||
: {}),
|
||||
...(max_tokens ? { max_tokens } : {}),
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
...(params || {}),
|
||||
},
|
||||
signal,
|
||||
candidate_key: candidate.key,
|
||||
|
||||
@@ -35,6 +35,7 @@ describe("openRouterModelToCatalog", () => {
|
||||
context_length: 1000000,
|
||||
max_output_tokens: 65536,
|
||||
voices: undefined,
|
||||
supported_parameters: ["tools", "temperature"],
|
||||
released_at: 1792000000000,
|
||||
expires_at: Date.parse("2026-12-01"),
|
||||
intelligence_score: 26.5,
|
||||
|
||||
@@ -56,6 +56,9 @@ export default function openRouterModelToCatalog({
|
||||
voices: model.supported_voices?.length
|
||||
? model.supported_voices
|
||||
: undefined,
|
||||
supported_parameters: model.supported_parameters?.length
|
||||
? model.supported_parameters
|
||||
: undefined,
|
||||
released_at: model.created ? model.created * 1000 : undefined,
|
||||
expires_at: Number.isNaN(expires_at) ? undefined : expires_at,
|
||||
intelligence_score: scores?.intelligence_index ?? undefined,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
HarnessMessage,
|
||||
HarnessToolDefinition,
|
||||
} from "../types";
|
||||
import grabChatParams from "./grab-chat-params";
|
||||
import prepareMessagesForModel from "./prepare-messages-for-model";
|
||||
import stripThinkTags from "./strip-think-tags";
|
||||
|
||||
@@ -23,7 +24,10 @@ export type GenerateTextParams = {
|
||||
*/
|
||||
prefer_key?: string;
|
||||
max_tokens?: number;
|
||||
temperature?: number;
|
||||
/**
|
||||
* Overrides the saved generation settings (media reading wants 0).
|
||||
*/
|
||||
params?: Record<string, number | string | { effort: string }>;
|
||||
signal?: AbortSignal;
|
||||
/**
|
||||
* Media readings shared across calls of one run.
|
||||
@@ -54,16 +58,22 @@ export default async function generateText({
|
||||
candidates,
|
||||
prefer_key,
|
||||
max_tokens,
|
||||
temperature,
|
||||
params,
|
||||
signal = new AbortController().signal,
|
||||
media_cache = new Map(),
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: GenerateTextParams): Promise<GenerateTextResult> {
|
||||
const resolved =
|
||||
candidates ||
|
||||
(await resolveHarnessCandidates({ harness: "text", bot_id, workspace_id }));
|
||||
const saved_params = params
|
||||
? undefined
|
||||
: await grabChatParams({ harness: "text", bot_id, workspace_id });
|
||||
|
||||
const { result, candidate } = await withCandidates({
|
||||
harness: "text",
|
||||
candidates:
|
||||
candidates || (await resolveHarnessCandidates({ harness: "text", bot_id, workspace_id })),
|
||||
candidates: resolved,
|
||||
prefer_key,
|
||||
run: async (candidate) => {
|
||||
const answer = await grabHarnessAdapter({
|
||||
@@ -80,7 +90,7 @@ export default async function generateText({
|
||||
}),
|
||||
tools,
|
||||
max_tokens,
|
||||
temperature,
|
||||
params: params || saved_params?.(candidate.model),
|
||||
signal,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
HarnessParameters,
|
||||
modelTakesParameter,
|
||||
} from "@/src/dict/harness-parameter-dict";
|
||||
import grabHarnessSetting from "@/src/functions/backend/db/harness/grab-harness-setting";
|
||||
import type { HarnessModelInfo } from "../types";
|
||||
|
||||
type Params = {
|
||||
harness: "text" | "vision";
|
||||
bot_id?: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export default async function grabChatParams({
|
||||
harness,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<
|
||||
(model: HarnessModelInfo) => Record<string, number | string | { effort: string }>
|
||||
> {
|
||||
const setting = await grabHarnessSetting({
|
||||
harness,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
});
|
||||
|
||||
return (model) =>
|
||||
Object.fromEntries(
|
||||
HarnessParameters.flatMap((parameter) => {
|
||||
const value = setting.options[parameter.key];
|
||||
|
||||
if (typeof value != "number" && typeof value != "string") return [];
|
||||
if (!value && value !== 0) return [];
|
||||
if (
|
||||
!modelTakesParameter({
|
||||
parameter,
|
||||
supported: model.supported_parameters,
|
||||
})
|
||||
)
|
||||
return [];
|
||||
|
||||
const key = "wire" in parameter ? parameter.wire : parameter.key;
|
||||
|
||||
return [
|
||||
[key, key == "reasoning" ? { effort: String(value) } : value],
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
+11
-1
@@ -82,6 +82,12 @@ export type HarnessModelInfo = {
|
||||
context_length?: number;
|
||||
max_output_tokens?: number;
|
||||
voices?: string[];
|
||||
/**
|
||||
* The request fields the provider says it takes (OpenRouter's
|
||||
* `supported_parameters`). Undefined when the provider doesn't say:
|
||||
* then every generation setting is sent.
|
||||
*/
|
||||
supported_parameters?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -137,7 +143,11 @@ export type HarnessChatParams = {
|
||||
messages: HarnessMessage[];
|
||||
tools?: HarnessToolDefinition[];
|
||||
max_tokens?: number;
|
||||
temperature?: number;
|
||||
/**
|
||||
* Sampling and reasoning fields, keyed by their wire name. The caller
|
||||
* only includes what the model advertises.
|
||||
*/
|
||||
params?: Record<string, number | string | { effort: string }>;
|
||||
signal: AbortSignal;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user