Files
cobble/src/pages/api/admin/bots/save-harness-settings.ts
T
tben c2553a4746 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.
2026-10-22 04:52:55 +01:00

106 lines
3.9 KiB
TypeScript

import userAuth from "@/src/functions/backend/auth/user-auth";
import grabUserBot from "@/src/functions/backend/db/bots/grab-user-bot";
import grabBotHarnessOverview from "@/src/functions/backend/db/harness/grab-bot-harness-overview";
import grabHarnessSetting from "@/src/functions/backend/db/harness/grab-harness-setting";
import saveBotHarnessSetting from "@/src/functions/backend/db/harness/save-bot-harness-setting";
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
import { withHarnessParameters } from "@/src/dict/harness-parameter-dict";
import { SaveBotHarnessSettingsRequestSchema } from "@/src/schemas/harness-schema";
import type { BotHarnessOverviewPayload } from "@/src/types";
import AppError from "@/src/utils/app-error";
import parseWithSchema from "@/src/utils/parse-with-schema";
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
import safeErrorMessage from "@/src/utils/safe-error-message";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
/**
* Saves which model each harness uses for one of the session user's
* worker bots. The coordinator always uses the defaults.
*/
export const handler: BunextAPIRouteHandler<
APIResponseObject<BotHarnessOverviewPayload>
> = async ({ req, body }) => {
try {
if (req.method != "POST") {
throw new AppError(`Method not allowed`);
}
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status || !user.id) {
return { success: false, msg: `Unauthorized`, logoutUser: true };
}
if (isReadOnlyUser({ user_types: user_types || [] })) {
throw new AppError(`This user can only read records.`);
}
if (!canManageCrew({ user_types })) {
throw new AppError(`Only an admin can do that.`);
}
const { bot_id, settings } = parseWithSchema({
schema: SaveBotHarnessSettingsRequestSchema,
data: body,
});
const bot = await grabUserBot({ bot_id, user_id: user.id });
if (!bot?.id) {
throw new AppError(`Bot not found.`);
}
if (bot.kind == "coordinator") {
throw new AppError(
`The coordinator uses the default models. A super admin can change them in Settings, under Models.`,
);
}
for (const setting of settings) {
const existing = await grabHarnessSetting({ harness: setting.harness, bot_id: bot.id });
const { voice, ...options } = existing.source == "bot" ? existing.options : {};
await saveBotHarnessSetting({
bot_id: bot.id,
harness: setting.harness,
inherit: setting.inherit,
model_id: setting.model_id,
provider_id: setting.provider_id,
fallback: setting.fallback,
enabled: setting.enabled,
options: withHarnessParameters({
options: {
...options,
...(setting.voice ? { voice: setting.voice } : {}),
},
params: setting.params || {},
}),
});
}
await logEvent({
title: "Bot model settings changed",
category: "system",
user_id: user.id,
plain_text: `User #${user.id} changed ${bot.name}'s model settings for ${settings.map((s) => s.harness).join(", ")}.`,
metadata: { bot_id: bot.id, settings },
ip_address: grabRequestIP(req),
});
return {
success: true,
msg: `Saved.`,
singleRes: await grabBotHarnessOverview({ bot_id: bot.id }),
};
} catch (error: unknown) {
return {
success: false,
msg: safeErrorMessage({ error }),
};
}
};