feat: no silent model fallback — explicit failure, gone models, per-profile fallback (#2385)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Label } from "@kandev/ui/label";
|
||||
import { Input } from "@kandev/ui/input";
|
||||
import { Switch } from "@kandev/ui/switch";
|
||||
@@ -10,6 +11,7 @@ import { useAppStore } from "@/components/state-provider";
|
||||
import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents";
|
||||
import { ModelCombobox } from "@/components/settings/model-combobox";
|
||||
import { ModeCombobox } from "@/components/settings/mode-combobox";
|
||||
import { ModelFallbackFields } from "@/components/agent/cli-profile-fallback-fields";
|
||||
import { CLIFlagsField } from "@/components/settings/cli-flags-field";
|
||||
import {
|
||||
createAgentAction,
|
||||
@@ -22,7 +24,6 @@ import {
|
||||
buildDefaultPermissions,
|
||||
PERMISSION_APPLY_AGENTCTL_AUTO_APPROVE,
|
||||
} from "@/lib/agent-permissions";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type CliProfileEditorMode = "create" | "edit";
|
||||
|
||||
@@ -51,6 +52,8 @@ type FormState = {
|
||||
agentName: string;
|
||||
profileName: string;
|
||||
model: string;
|
||||
fallbackModel: string;
|
||||
autoFallback: boolean;
|
||||
mode: string;
|
||||
cliFlags: CLIFlag[];
|
||||
cliPassthrough: boolean;
|
||||
@@ -67,6 +70,8 @@ function fromExistingProfile(profile: AgentProfile): FormState {
|
||||
agentName: profile.agentId ?? "",
|
||||
profileName: profile.name,
|
||||
model: profile.model ?? "",
|
||||
fallbackModel: profile.fallbackModel ?? "",
|
||||
autoFallback: profile.autoFallback ?? false,
|
||||
mode: profile.mode ?? "",
|
||||
cliFlags: profile.cliFlags ?? [],
|
||||
cliPassthrough: profile.cliPassthrough ?? false,
|
||||
@@ -86,6 +91,8 @@ function fromDefaultAgent(
|
||||
agentName: defaultAgent?.name ?? "",
|
||||
profileName: defaultName,
|
||||
model: cfg?.default_model ?? "",
|
||||
fallbackModel: "",
|
||||
autoFallback: false,
|
||||
mode: cfg?.current_mode_id ?? "",
|
||||
cliFlags: seedDefaultCLIFlags(permissionSettings),
|
||||
cliPassthrough: false,
|
||||
@@ -180,6 +187,10 @@ export function CliProfileEditor({
|
||||
patch({
|
||||
agentName: name,
|
||||
model: installed.find((a) => a.name === name)?.model_config.default_model ?? "",
|
||||
// A fallback model belongs to the previous client's model
|
||||
// vocabulary; carrying it over would save a false gone-model
|
||||
// configuration for the new client.
|
||||
fallbackModel: "",
|
||||
mode: installed.find((a) => a.name === name)?.model_config.current_mode_id ?? "",
|
||||
cliFlags: seedDefaultCLIFlags(
|
||||
installed.find((a) => a.name === name)?.permission_settings ?? {},
|
||||
@@ -194,13 +205,7 @@ export function CliProfileEditor({
|
||||
onChange={(profileName) => patch({ profileName })}
|
||||
/>
|
||||
|
||||
<ModelModeFields
|
||||
modelConfig={modelConfig ?? null}
|
||||
model={form.model}
|
||||
mode={form.mode}
|
||||
onModelChange={(v) => patch({ model: v })}
|
||||
onModeChange={(v) => patch({ mode: v })}
|
||||
/>
|
||||
<ModelModeFieldsBinding form={form} patch={patch} modelConfig={modelConfig ?? null} />
|
||||
|
||||
<AdvancedToggles
|
||||
open={advancedOpen}
|
||||
@@ -307,47 +312,108 @@ function CliClientPicker({
|
||||
);
|
||||
}
|
||||
|
||||
// ModelModeFieldsBinding forwards the editor's form state to ModelModeFields.
|
||||
// Extracted so CliProfileEditor stays under the linter's line budget.
|
||||
function ModelModeFieldsBinding({
|
||||
form,
|
||||
patch,
|
||||
modelConfig,
|
||||
}: {
|
||||
form: FormState;
|
||||
patch: (p: Partial<FormState>) => void;
|
||||
modelConfig: NonNullable<AvailableAgent["model_config"]> | null;
|
||||
}) {
|
||||
return (
|
||||
<ModelModeFields
|
||||
modelConfig={modelConfig}
|
||||
model={form.model}
|
||||
fallbackModel={form.fallbackModel}
|
||||
autoFallback={form.autoFallback}
|
||||
mode={form.mode}
|
||||
onModelChange={(v) => patch({ model: v })}
|
||||
onFallbackModelChange={(v) => patch({ fallbackModel: v })}
|
||||
onAutoFallbackChange={(v) => patch({ autoFallback: v })}
|
||||
onModeChange={(v) => patch({ mode: v })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type ModelModeFieldsProps = {
|
||||
modelConfig: NonNullable<AvailableAgent["model_config"]> | null;
|
||||
model: string;
|
||||
fallbackModel: string;
|
||||
autoFallback: boolean;
|
||||
mode: string;
|
||||
onModelChange: (v: string) => void;
|
||||
onFallbackModelChange: (v: string) => void;
|
||||
onAutoFallbackChange: (v: boolean) => void;
|
||||
onModeChange: (v: string) => void;
|
||||
};
|
||||
|
||||
function ModelModeFields({
|
||||
modelConfig,
|
||||
model,
|
||||
fallbackModel,
|
||||
autoFallback,
|
||||
mode,
|
||||
onModelChange,
|
||||
onFallbackModelChange,
|
||||
onAutoFallbackChange,
|
||||
onModeChange,
|
||||
}: ModelModeFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
if (!modelConfig) {
|
||||
return <p className="text-xs text-muted-foreground">{t("common:pickACliClientToLoad")}</p>;
|
||||
}
|
||||
const availableModels = modelConfig.available_models ?? [];
|
||||
const startModelGone = Boolean(model && !availableModels.some((m) => m.id === model));
|
||||
const fallbackModelGone = Boolean(
|
||||
fallbackModel && !availableModels.some((m) => m.id === fallbackModel),
|
||||
);
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>{t("common:model")}</Label>
|
||||
<ModelCombobox
|
||||
value={model}
|
||||
onChange={onModelChange}
|
||||
models={modelConfig.available_models ?? []}
|
||||
currentModelId={modelConfig.current_model_id}
|
||||
/>
|
||||
</div>
|
||||
{(modelConfig.available_modes ?? []).length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>{t("common:mode")}</Label>
|
||||
<ModeCombobox
|
||||
value={mode}
|
||||
onChange={onModeChange}
|
||||
modes={modelConfig.available_modes ?? []}
|
||||
currentModeId={modelConfig.current_mode_id}
|
||||
<Label>{t("common:model")}</Label>
|
||||
<ModelCombobox
|
||||
value={model}
|
||||
onChange={onModelChange}
|
||||
models={
|
||||
startModelGone
|
||||
? [
|
||||
...availableModels,
|
||||
{
|
||||
id: model,
|
||||
name: `${model} (${t("settings:startModelUnavailable")})`,
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
: availableModels
|
||||
}
|
||||
currentModelId={modelConfig.current_model_id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(modelConfig.available_modes ?? []).length > 0 && (
|
||||
<div>
|
||||
<Label>{t("common:mode")}</Label>
|
||||
<ModeCombobox
|
||||
value={mode}
|
||||
onChange={onModeChange}
|
||||
modes={modelConfig.available_modes ?? []}
|
||||
currentModeId={modelConfig.current_mode_id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ModelFallbackFields
|
||||
availableModels={availableModels}
|
||||
fallbackModel={fallbackModel}
|
||||
fallbackModelGone={fallbackModelGone}
|
||||
autoFallback={autoFallback}
|
||||
currentModelId={modelConfig.current_model_id}
|
||||
onFallbackModelChange={onFallbackModelChange}
|
||||
onAutoFallbackChange={onAutoFallbackChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -500,6 +566,8 @@ async function saveExistingProfile(id: string, form: FormState): Promise<AgentPr
|
||||
return updateAgentProfileAction(id, {
|
||||
name: form.profileName.trim(),
|
||||
model: form.model,
|
||||
fallback_model: form.fallbackModel ?? "",
|
||||
auto_fallback: form.autoFallback,
|
||||
mode: form.mode || undefined,
|
||||
allow_indexing: form.allowIndexing,
|
||||
auto_approve: form.autoApprove,
|
||||
@@ -513,6 +581,8 @@ async function saveNewProfile(form: FormState, settingsAgents: Agent[]): Promise
|
||||
const profilePayload = {
|
||||
name: form.profileName.trim(),
|
||||
model: form.model,
|
||||
fallback_model: form.fallbackModel ?? "",
|
||||
auto_fallback: form.autoFallback,
|
||||
mode: form.mode || undefined,
|
||||
allow_indexing: form.allowIndexing,
|
||||
auto_approve: form.autoApprove,
|
||||
|
||||
Reference in New Issue
Block a user