feat: chatbot model setting + tool-calling toggle, simplify admin AI config (v0.59.0)

Chatbot (general assistant + per-recipe Q&A) now resolves its default model
from its own site setting (DEFAULT_CHAT_PROVIDER/MODEL) instead of sharing
the generic "text" use case with recipe generation, so admins can point it
at a different model.

Added AI_TOOL_CALLING_ENABLED: turns off the chatbot's createRecipe/
addToShoppingList tools (and the forced-retry pass) for models that don't
reliably support tool calling — it falls back to plain text answers.

Simplified the admin AI Configuration page: it showed provider keys and
routing settings twice (a read-only card, then an identical edit form).
Merged into one editable section; the resolved fallback provider is now a
one-line note instead of its own card.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-20 21:14:04 +02:00
parent f6975e98a9
commit 623e5bcd34
10 changed files with 120 additions and 113 deletions
+2 -1
View File
@@ -3,12 +3,13 @@ import { decrypt } from "@/lib/encrypt";
import { getSiteSetting, type SiteSettingKey } from "@/lib/site-settings";
import type { AiConfig, AiProvider } from "./factory";
export type ModelUseCase = "text" | "vision" | "mealPlan";
export type ModelUseCase = "text" | "vision" | "mealPlan" | "chat";
const USE_CASE_SITE_DEFAULTS: Record<ModelUseCase, { provider: SiteSettingKey; model: SiteSettingKey }> = {
text: { provider: "DEFAULT_TEXT_PROVIDER", model: "DEFAULT_TEXT_MODEL" },
vision: { provider: "DEFAULT_VISION_PROVIDER", model: "DEFAULT_VISION_MODEL" },
mealPlan: { provider: "DEFAULT_MEAL_PLAN_PROVIDER", model: "DEFAULT_MEAL_PLAN_MODEL" },
chat: { provider: "DEFAULT_CHAT_PROVIDER", model: "DEFAULT_CHAT_MODEL" },
};
const PROVIDER_API_KEY_SETTING: Record<AiProvider, SiteSettingKey | null> = {
+12 -1
View File
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together.
export const APP_VERSION = "0.58.0";
export const APP_VERSION = "0.59.0";
export type ChangelogEntry = {
version: string;
@@ -11,6 +11,17 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.59.0",
date: "2026-07-20 21:15",
added: [
"Admin: chatbot now has its own default-model setting, separate from generic text generation — the general assistant and per-recipe Q&A chat can be pointed at a different model than recipe generation.",
"Admin: tool calling (the chatbot's inline recipe/shopping-list drafting) can be toggled off — useful for a local/Ollama model that doesn't reliably support it, so the bot falls back to plain text answers instead.",
],
fixed: [
"Simplified the admin AI Configuration page — it showed the same provider keys and routing settings twice (once read-only, once as an edit form). Merged into one section.",
],
},
{
version: "0.58.0",
date: "2026-07-20 21:00",
+4 -1
View File
@@ -778,12 +778,15 @@ export function generateOpenApiSpec(): object {
NEXT_PUBLIC_VAPID_PUBLIC_KEY: z.string().nullable().optional(),
VAPID_PRIVATE_KEY: z.string().nullable().optional().describe("secret — write-only, never returned"),
SIGNUPS_DISABLED: z.string().nullable().optional(),
DEFAULT_TEXT_PROVIDER: z.enum(["openai", "anthropic", "openrouter", "ollama"]).nullable().optional().describe("Site-wide default for recipe/chat text generation when a user has no personal model preference and no BYOK key."),
DEFAULT_TEXT_PROVIDER: z.enum(["openai", "anthropic", "openrouter", "ollama"]).nullable().optional().describe("Site-wide default for recipe generation and other text use cases when a user has no personal model preference and no BYOK key."),
DEFAULT_TEXT_MODEL: z.string().nullable().optional(),
DEFAULT_CHAT_PROVIDER: z.enum(["openai", "anthropic", "openrouter", "ollama"]).nullable().optional().describe("Site-wide default for the chatbot (general cooking assistant + per-recipe Q&A)."),
DEFAULT_CHAT_MODEL: z.string().nullable().optional(),
DEFAULT_VISION_PROVIDER: z.enum(["openai", "anthropic", "openrouter", "ollama"]).nullable().optional().describe("Site-wide default for photo/vision use cases (pantry scan, photo import)."),
DEFAULT_VISION_MODEL: z.string().nullable().optional(),
DEFAULT_MEAL_PLAN_PROVIDER: z.enum(["openai", "anthropic", "openrouter", "ollama"]).nullable().optional().describe("Site-wide default for AI meal-plan generation."),
DEFAULT_MEAL_PLAN_MODEL: z.string().nullable().optional(),
AI_TOOL_CALLING_ENABLED: z.string().nullable().optional().describe("\"false\" disables the chatbot's createRecipe/addToShoppingList tools (e.g. for a model that can't reliably call tools); any other value or unset means enabled."),
}).describe("Unknown keys are silently ignored. Setting a key to null or \"\" deletes it (falls back to env var)."));
const TestEmailBodyRef = registry.register("TestEmailBody", z.object({ to: z.string().min(1) }));
+13
View File
@@ -16,6 +16,9 @@ export type SiteSettingKey =
| "DEFAULT_VISION_MODEL"
| "DEFAULT_MEAL_PLAN_PROVIDER"
| "DEFAULT_MEAL_PLAN_MODEL"
| "DEFAULT_CHAT_PROVIDER"
| "DEFAULT_CHAT_MODEL"
| "AI_TOOL_CALLING_ENABLED"
| "GITEA_URL"
| "GITEA_TOKEN"
| "GITEA_REPO";
@@ -62,6 +65,9 @@ export async function getAllSiteSettings(): Promise<Record<string, { value: stri
"DEFAULT_VISION_MODEL",
"DEFAULT_MEAL_PLAN_PROVIDER",
"DEFAULT_MEAL_PLAN_MODEL",
"DEFAULT_CHAT_PROVIDER",
"DEFAULT_CHAT_MODEL",
"AI_TOOL_CALLING_ENABLED",
"GITEA_URL",
"GITEA_TOKEN",
"GITEA_REPO",
@@ -93,6 +99,13 @@ export async function isSignupsDisabled(): Promise<boolean> {
return (await getSiteSetting("SIGNUPS_DISABLED")) === "true";
}
/** Defaults to enabled — the chatbot's createRecipe/addToShoppingList tools
* only get turned off explicitly, e.g. for a local model that can't reliably
* call tools. */
export async function isAiToolCallingEnabled(): Promise<boolean> {
return (await getSiteSetting("AI_TOOL_CALLING_ENABLED")) !== "false";
}
export async function setSiteSetting(
key: SiteSettingKey,
value: string | null,