feat: Support editing all parameters individually for each model (#1562)

This commit is contained in:
Emt-lin 2025-06-28 06:57:12 +08:00 committed by GitHub
parent aaa53c248a
commit d6b594dcfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 572 additions and 308 deletions

View file

@ -122,7 +122,11 @@ export default class ChatModelManager {
fetch: customModel.enableCors ? safeFetch : undefined,
organization: await getDecryptedKey(customModel.openAIOrgId || settings.openAIOrgId),
},
...this.handleOpenAIExtraArgs(isOSeries, settings.maxTokens, settings.temperature),
...this.handleOpenAIExtraArgs(
isOSeries,
customModel.maxTokens ?? settings.maxTokens,
customModel.temperature ?? settings.temperature
),
},
[ChatModelProviders.ANTHROPIC]: {
anthropicApiKey: await getDecryptedKey(customModel.apiKey || settings.anthropicApiKey),
@ -156,7 +160,11 @@ export default class ChatModelManager {
},
fetch: customModel.enableCors ? safeFetch : undefined,
},
...this.handleOpenAIExtraArgs(isOSeries, settings.maxTokens, settings.temperature),
...this.handleOpenAIExtraArgs(
isOSeries,
customModel.maxTokens ?? settings.maxTokens,
customModel.temperature ?? settings.temperature
),
},
[ChatModelProviders.COHEREAI]: {
apiKey: await getDecryptedKey(customModel.apiKey || settings.cohereApiKey),
@ -231,7 +239,11 @@ export default class ChatModelManager {
fetch: customModel.enableCors ? safeFetch : undefined,
defaultHeaders: { "dangerously-allow-browser": "true" },
},
...this.handleOpenAIExtraArgs(isOSeries, settings.maxTokens, settings.temperature),
...this.handleOpenAIExtraArgs(
isOSeries,
customModel.maxTokens ?? settings.maxTokens,
customModel.temperature ?? settings.temperature
),
},
[ChatModelProviders.COPILOT_PLUS]: {
modelName: modelName,
@ -259,7 +271,12 @@ export default class ChatModelManager {
const selectedProviderConfig =
providerConfig[customModel.provider as keyof typeof providerConfig] || {};
// Add token configuration separately to ensure they don't conflict
// Get provider-specific parameters (like topP, frequencyPenalty) that the provider supports
const providerSpecificParams = this.getProviderSpecificParams(
customModel.provider as ChatModelProviders,
customModel
);
const tokenConfig = isThinkingEnabled
? {
maxTokens: customModel.maxTokens ?? settings.maxTokens,
@ -273,6 +290,7 @@ export default class ChatModelManager {
const finalConfig = {
...baseConfig,
...selectedProviderConfig,
...providerSpecificParams,
...tokenConfig,
};
@ -301,6 +319,56 @@ export default class ChatModelManager {
return config;
}
/**
* Returns provider-specific parameters (like topP, frequencyPenalty) based on what the provider supports
* This prevents passing undefined values to providers that don't support them
*/
private getProviderSpecificParams(provider: ChatModelProviders, customModel: CustomModel) {
const params: Record<string, any> = {};
// Add topP only if defined
if (customModel.topP !== undefined) {
// These providers support topP
if (
[
ChatModelProviders.OPENAI,
ChatModelProviders.AZURE_OPENAI,
ChatModelProviders.ANTHROPIC,
ChatModelProviders.GOOGLE,
ChatModelProviders.OPENROUTERAI,
ChatModelProviders.OLLAMA,
ChatModelProviders.LM_STUDIO,
ChatModelProviders.OPENAI_FORMAT,
ChatModelProviders.MISTRAL,
ChatModelProviders.DEEPSEEK,
].includes(provider)
) {
params.topP = customModel.topP;
}
}
// Add frequencyPenalty only if defined
if (customModel.frequencyPenalty !== undefined) {
// These providers support frequencyPenalty
if (
[
ChatModelProviders.OPENAI,
ChatModelProviders.AZURE_OPENAI,
ChatModelProviders.OPENROUTERAI,
ChatModelProviders.OLLAMA,
ChatModelProviders.LM_STUDIO,
ChatModelProviders.OPENAI_FORMAT,
ChatModelProviders.MISTRAL,
ChatModelProviders.DEEPSEEK,
].includes(provider)
) {
params.frequencyPenalty = customModel.frequencyPenalty;
}
}
return params;
}
// Build a map of modelKey to model config
public buildModelMap() {
const activeModels = getSettings().activeModels;

View file

@ -105,8 +105,9 @@ export interface CustomModel {
stream?: boolean;
temperature?: number;
maxTokens?: number;
topP?: number;
frequencyPenalty?: number;
context?: number;
projectEnabled?: boolean;
plusExclusive?: boolean;
believerExclusive?: boolean;

View file

@ -16,6 +16,7 @@ import { HelpCircle } from "lucide-react";
import { App, Modal, Notice } from "obsidian";
import React, { useState } from "react";
import { createRoot, Root } from "react-dom/client";
import { DEFAULT_MODEL_SETTING } from "@/constants";
interface AddProjectModalContentProps {
initialProject?: ProjectConfig;
@ -41,8 +42,8 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
systemPrompt: "",
projectModelKey: "",
modelConfigs: {
temperature: 1.0,
maxTokens: 1000,
temperature: DEFAULT_MODEL_SETTING.TEMPERATURE,
maxTokens: DEFAULT_MODEL_SETTING.MAX_TOKENS,
},
contextSource: {
inclusions: "",
@ -231,7 +232,7 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
<div className="tw-grid tw-grid-cols-1 tw-gap-4">
<FormField label="Temperature">
<SettingSlider
value={formData.modelConfigs?.temperature ?? 1}
value={formData.modelConfigs?.temperature ?? DEFAULT_MODEL_SETTING.TEMPERATURE}
onChange={(value) => handleInputChange("modelConfigs.temperature", value)}
min={0}
max={2}
@ -241,7 +242,7 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
</FormField>
<FormField label="Token Limit">
<SettingSlider
value={formData.modelConfigs?.maxTokens ?? 1000}
value={formData.modelConfigs?.maxTokens ?? DEFAULT_MODEL_SETTING.MAX_TOKENS}
onChange={(value) => handleInputChange("modelConfigs.maxTokens", value)}
min={1}
max={65000}

View file

@ -32,9 +32,9 @@ const ScrollBar = React.forwardRef<
className={cn(
"tw-flex tw-touch-none tw-select-none tw-transition-colors",
orientation === "vertical" &&
"tw-h-full tw-w-2.5 tw-border-l tw-border-l-transparent tw-p-[1px]",
"tw-h-full tw-w-2.5 tw-border-l tw-border-l-transparent tw-p-px",
orientation === "horizontal" &&
"tw-h-2.5 tw-flex-col tw-border-t tw-border-t-transparent tw-p-[1px]",
"tw-h-2.5 tw-flex-col tw-border-t tw-border-t-transparent tw-p-px",
className
)}
{...props}

View file

@ -16,7 +16,7 @@ const Separator = React.forwardRef<
orientation={orientation}
className={cn(
"tw-shrink-0 tw-bg-[var(--background-modifier-border)]",
orientation === "horizontal" ? "tw-h-[1px] tw-w-full" : "tw-h-full tw-w-[1px]",
orientation === "horizontal" ? "tw-h-px tw-w-full" : "tw-h-full tw-w-px",
className
)}
{...props}

View file

@ -108,6 +108,11 @@ export const PLUS_UTM_MEDIUMS = {
};
export type PlusUtmMedium = (typeof PLUS_UTM_MEDIUMS)[keyof typeof PLUS_UTM_MEDIUMS];
export const DEFAULT_MODEL_SETTING = {
MAX_TOKENS: 6000,
TEMPERATURE: 0.1,
};
export enum ChatModels {
COPILOT_PLUS_FLASH = "copilot-plus-flash",
GPT_41 = "gpt-4.1",
@ -653,8 +658,8 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
defaultChainType: ChainType.LLM_CHAIN,
defaultModelKey: ChatModels.GPT_41 + "|" + ChatModelProviders.OPENAI,
embeddingModelKey: EmbeddingModels.OPENAI_EMBEDDING_SMALL + "|" + EmbeddingModelProviders.OPENAI,
temperature: 0.1,
maxTokens: 6000,
temperature: DEFAULT_MODEL_SETTING.TEMPERATURE,
maxTokens: DEFAULT_MODEL_SETTING.MAX_TOKENS,
contextTurns: 15,
userSystemPrompt: "",
openAIProxyBaseUrl: "",

View file

@ -1,18 +1,12 @@
import { CustomModel } from "@/aiParams";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { FormField } from "@/components/ui/form-field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
DEFAULT_MODEL_SETTING,
MODEL_CAPABILITIES,
ModelCapability,
Provider,
@ -20,28 +14,33 @@ import {
ProviderSettingsKeyMap,
SettingKeyProviders,
} from "@/constants";
import { useTab } from "@/contexts/TabContext";
import { getSettings } from "@/settings/model";
import { debounce, getProviderInfo, getProviderLabel } from "@/utils";
import { HelpCircle } from "lucide-react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { SettingSlider } from "@/components/ui/setting-slider";
import { App, Modal } from "obsidian";
import { createRoot, Root } from "react-dom/client";
interface ModelEditDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: CustomModel | null;
onUpdate: (originalModel: CustomModel, updatedModel: CustomModel) => void;
interface ModelEditModalContentProps {
model: CustomModel;
isEmbeddingModel: boolean;
onUpdate: (
isEmbeddingModel: boolean,
originalModel: CustomModel,
updatedModel: CustomModel
) => void;
onCancel: () => void;
}
export const ModelEditDialog: React.FC<ModelEditDialogProps> = ({
open,
onOpenChange,
export const ModelEditModalContent: React.FC<ModelEditModalContentProps> = ({
model,
onUpdate,
onCancel,
isEmbeddingModel,
}) => {
const { modalContainer } = useTab();
const [localModel, setLocalModel] = useState<CustomModel | null>(model);
const [originalModel, setOriginalModel] = useState<CustomModel | null>(model);
const [localModel, setLocalModel] = useState<CustomModel>(model);
const [originalModel, setOriginalModel] = useState<CustomModel>(model);
const [providerInfo, setProviderInfo] = useState<ProviderMetadata>({} as ProviderMetadata);
const settings = getSettings();
@ -52,7 +51,7 @@ export const ModelEditDialog: React.FC<ModelEditDialogProps> = ({
useEffect(() => {
setLocalModel(model);
setOriginalModel(model);
if (model?.provider) {
if (model.provider) {
setProviderInfo(getProviderInfo(model.provider));
}
}, [model]);
@ -60,19 +59,16 @@ export const ModelEditDialog: React.FC<ModelEditDialogProps> = ({
// Debounce the onUpdate callback
const debouncedOnUpdate = useMemo(
() =>
debounce((currentOriginalModel: CustomModel | null, updatedModel: CustomModel) => {
if (currentOriginalModel) {
onUpdate(currentOriginalModel, updatedModel);
}
debounce((currentOriginalModel: CustomModel, updatedModel: CustomModel) => {
onUpdate(isEmbeddingModel, currentOriginalModel, updatedModel);
}, 500),
[onUpdate]
[isEmbeddingModel, onUpdate]
);
// Function to update local state immediately
const handleLocalUpdate = useCallback(
(field: keyof CustomModel, value: any) => {
setLocalModel((prevModel) => {
if (!prevModel) return null;
const updatedModel = {
...prevModel,
[field]: value,
@ -109,147 +105,162 @@ export const ModelEditDialog: React.FC<ModelEditDialogProps> = ({
})) as Array<{ id: ModelCapability; label: string; description: string }>;
const displayApiKey = localModel.apiKey || getDefaultApiKey(localModel.provider as Provider);
const showOtherParameters =
!isEmbeddingModel &&
localModel.provider !== "copilot-plus" &&
localModel.provider !== "copilot-plus-jina";
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:tw-max-w-[425px]" container={modalContainer}>
<DialogHeader>
<DialogTitle>Model Settings - {localModel.name}</DialogTitle>
<DialogDescription>Customize model parameters.</DialogDescription>
</DialogHeader>
<div className="tw-space-y-3 tw-p-4">
<div className="tw-mb-4">
<h2 className="tw-text-xl tw-font-bold">Model Settings - {localModel.name}</h2>
<p className="tw-text-sm tw-text-muted">Customize model parameters</p>
</div>
<div className="tw-space-y-3">
<FormField label="Model Name" required>
<Input
type="text"
disabled={localModel.core}
value={localModel.name}
onChange={(e) => handleLocalUpdate("name", e.target.value)}
placeholder="Enter model name"
/>
</FormField>
<div className="tw-space-y-3">
<FormField label="Model Name" required>
<Input
type="text"
disabled={localModel.core}
value={localModel.name}
onChange={(e) => handleLocalUpdate("name", e.target.value)}
placeholder="Enter model name"
/>
</FormField>
<FormField
label={
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">Display Name</span>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="tw-size-4" />
</TooltipTrigger>
<TooltipContent align="start" className="tw-max-w-96" side="bottom">
<div className="tw-flex tw-flex-col tw-gap-0.5 tw-text-sm tw-text-muted">
<div className="tw-text-[12px] tw-font-bold">Suggested format:</div>
<div className="tw-text-accent">[Source]-[Payment]:[Pretty Model Name]</div>
<div className="tw-text-[12px]">
Example:
<li>Direct-Paid:Ds-r1</li>
<li>OpenRouter-Paid:Ds-r1</li>
<li>Perplexity-Paid:lg</li>
</div>
<FormField
label={
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">Display Name</span>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="tw-size-4" />
</TooltipTrigger>
<TooltipContent align="start" className="tw-max-w-96" side="bottom">
<div className="tw-flex tw-flex-col tw-gap-0.5 tw-text-sm tw-text-muted">
<div className="tw-text-[12px] tw-font-bold">Suggested format:</div>
<div className="tw-text-accent">[Source]-[Payment]:[Pretty Model Name]</div>
<div className="tw-text-[12px]">
Example:
<li>Direct-Paid:Ds-r1</li>
<li>OpenRouter-Paid:Ds-r1</li>
<li>Perplexity-Paid:lg</li>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
}
>
<Input
type="text"
placeholder="Custom display name (optional)"
value={localModel.displayName || ""}
onChange={(e) => handleLocalUpdate("displayName", e.target.value)}
/>
</FormField>
<FormField label="Provider">
<Input type="text" value={getProviderLabel(localModel.provider)} disabled />
</FormField>
<FormField label="Base URL" description="Leave it blank, unless you are using a proxy.">
<Input
type="text"
placeholder={getPlaceholderUrl()}
value={localModel.baseUrl || ""}
onChange={(e) => handleLocalUpdate("baseUrl", e.target.value)}
/>
</FormField>
<FormField label="API Key">
<PasswordInput
placeholder={`Enter ${providerInfo.label || "Provider"} API Key`}
value={displayApiKey}
onChange={(value) => handleLocalUpdate("apiKey", value)}
/>
{providerInfo.keyManagementURL && (
<p className="tw-text-xs tw-text-muted">
<a href={providerInfo.keyManagementURL} target="_blank" rel="noopener noreferrer">
Get {providerInfo.label} API Key
</a>
</p>
)}
</FormField>
<FormField
label={
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">Model Capabilities</span>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="tw-size-4" />
</TooltipTrigger>
<TooltipContent align="start" className="tw-max-w-96" side="bottom">
<div className="tw-text-sm tw-text-muted">
Only used to display model capabilities, does not affect model functionality
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
}
>
<div className="tw-flex tw-items-center tw-gap-4">
{capabilityOptions.map(({ id, label, description }) => (
<div key={id} className="tw-flex tw-items-center tw-gap-2">
<Checkbox
id={id}
checked={localModel.capabilities?.includes(id)}
onCheckedChange={(checked) => {
const newCapabilities = localModel.capabilities || [];
const value = checked
? [...newCapabilities, id]
: newCapabilities.filter((cap) => cap !== id);
handleLocalUpdate("capabilities", value);
}}
/>
<Label htmlFor={id} className="tw-text-sm">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<span>{label}</span>
</TooltipTrigger>
<TooltipContent side="bottom">{description}</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
</div>
))}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</FormField>
}
>
<Input
type="text"
placeholder="Custom display name (optional)"
value={localModel.displayName || ""}
onChange={(e) => handleLocalUpdate("displayName", e.target.value)}
/>
</FormField>
{/*<FormField
<FormField label="Provider">
<Input type="text" value={getProviderLabel(localModel.provider)} disabled />
</FormField>
<FormField label="Base URL" description="Leave it blank, unless you are using a proxy.">
<Input
type="text"
placeholder={getPlaceholderUrl()}
value={localModel.baseUrl || ""}
onChange={(e) => handleLocalUpdate("baseUrl", e.target.value)}
/>
</FormField>
<FormField label="API Key">
<PasswordInput
placeholder={`Enter ${providerInfo.label || "Provider"} API Key`}
value={displayApiKey}
onChange={(value) => handleLocalUpdate("apiKey", value)}
/>
{providerInfo.keyManagementURL && (
<p className="tw-text-xs tw-text-muted">
<a href={providerInfo.keyManagementURL} target="_blank" rel="noopener noreferrer">
Get {providerInfo.label} API Key
</a>
</p>
)}
</FormField>
{showOtherParameters && (
<>
<FormField
label={
<div className="tw-flex tw-items-center tw-gap-2">
Temperature
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">Model Capabilities</span>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="tw-h-4 tw-w-4 tw-text-muted" />
<HelpCircle className="tw-size-4" />
</TooltipTrigger>
<TooltipContent align="start" className="tw-max-w-96" side="bottom">
<div className="tw-text-sm tw-text-muted">
Only used to display model capabilities, does not affect model
functionality
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
}
>
<div className="tw-flex tw-items-center tw-gap-4">
{capabilityOptions.map(({ id, label, description }) => (
<div key={id} className="tw-flex tw-items-center tw-gap-2">
<Checkbox
id={id}
checked={localModel.capabilities?.includes(id)}
onCheckedChange={(checked) => {
const newCapabilities = localModel.capabilities || [];
const value = checked
? [...newCapabilities, id]
: newCapabilities.filter((cap) => cap !== id);
handleLocalUpdate("capabilities", value);
}}
/>
<Label htmlFor={id} className="tw-text-sm">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<span>{label}</span>
</TooltipTrigger>
<TooltipContent side="bottom">{description}</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
</div>
))}
</div>
</FormField>
<FormField
label={
<div className="tw-flex tw-items-center tw-gap-2">
Token limit
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="tw-size-4 tw-text-muted" />
</TooltipTrigger>
<TooltipContent side="bottom">
Controls randomness: 0 is focused and deterministic, 2 is more creative
<div className="tw-w-[300px]">
<p>
The maximum number of <em>output tokens</em> to generate. Default is{" "}
{DEFAULT_MODEL_SETTING.MAX_TOKENS}.
</p>
<em>
This number plus the length of your prompt (input tokens) must be
smaller than the context window of the model.
</em>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -257,25 +268,30 @@ export const ModelEditDialog: React.FC<ModelEditDialogProps> = ({
}
>
<SettingSlider
value={localModel.temperature ?? 0.1}
onChange={(value) => handleLocalUpdate("temperature", value)}
max={2}
value={
localModel.maxTokens ?? settings.maxTokens ?? DEFAULT_MODEL_SETTING.MAX_TOKENS
}
onChange={(value) => handleLocalUpdate("maxTokens", value)}
min={0}
step={0.1}
max={65000}
step={100}
/>
</FormField>
<FormField
label={
<div className="tw-flex tw-items-center tw-gap-2">
Context
Temperature
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="tw-h-4 tw-w-4 tw-text-muted" />
<HelpCircle className="tw-size-4 tw-text-muted" />
</TooltipTrigger>
<TooltipContent side="bottom">
Maximum number of tokens to use for context
<div className="tw-max-w-[300px]">
Default is {DEFAULT_MODEL_SETTING.TEMPERATURE}. Higher values will result
in more creativeness, but also more mistakes. Set to 0 for no randomness.
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -283,35 +299,132 @@ export const ModelEditDialog: React.FC<ModelEditDialogProps> = ({
}
>
<SettingSlider
value={localModel.context ?? 1000}
onChange={(value) => handleLocalUpdate("context", value)}
max={65000}
value={
localModel.temperature ??
settings.temperature ??
DEFAULT_MODEL_SETTING.TEMPERATURE
}
onChange={(value) => handleLocalUpdate("temperature", value)}
max={2}
min={0}
step={100}
step={0.05}
/>
</FormField>
<div className="tw-flex tw-items-center tw-justify-between tw-py-2">
<div className="tw-flex tw-items-center tw-gap-2">
<span className="tw-text-sm tw-font-medium">Stream output</span>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="tw-h-4 tw-w-4 tw-text-muted" />
</TooltipTrigger>
<TooltipContent side="bottom">
Enable streaming responses from the model
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<SettingSwitch
checked={localModel.stream ?? true}
onCheckedChange={(checked) => handleLocalUpdate("stream", checked)}
/>
</div>*/}
</div>
</DialogContent>
</Dialog>
<FormField
label={
<div className="tw-flex tw-items-center tw-gap-2">
Top-P
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="tw-size-4 tw-text-muted" />
</TooltipTrigger>
<TooltipContent side="bottom">
<div className="tw-w-[300px]">
Default value is 0.9, the smaller the value, the less variety in the
answers, the easier to understand, the larger the value, the larger the
range of the Al&#39;s vocabulary, the more diverse
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
}
>
<SettingSlider
value={localModel.topP ?? 0.9}
onChange={(value) => handleLocalUpdate("topP", value)}
max={1}
min={0}
step={0.05}
/>
</FormField>
<FormField
label={
<div className="tw-flex tw-items-center tw-gap-2">
Frequency Penalty
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="tw-size-4 tw-text-muted" />
</TooltipTrigger>
<TooltipContent side="bottom">
<div className="tw-w-[300px]">
<p>
The frequency penalty parameter tells the model not to repeat a word
that has already been used multiple times in the conversation.
</p>
<em>
The higher the value, the more the model is penalized for repeating
words.
</em>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
}
>
<SettingSlider
value={localModel.frequencyPenalty ?? 0}
onChange={(value) => handleLocalUpdate("frequencyPenalty", value)}
max={2}
min={0}
step={0.05}
/>
</FormField>
</>
)}
</div>
</div>
);
};
export class ModelEditModal extends Modal {
private root: Root;
constructor(
app: App,
private model: CustomModel,
private isEmbeddingModel: boolean,
private onUpdate: (
isEmbeddingModel: boolean,
originalModel: CustomModel,
updatedModel: CustomModel
) => void
) {
super(app);
}
onOpen() {
const { contentEl } = this;
this.root = createRoot(contentEl);
const handleUpdate = (
isEmbeddingModel: boolean,
originalModel: CustomModel,
updatedModel: CustomModel
) => {
this.onUpdate(isEmbeddingModel, originalModel, updatedModel);
};
const handleCancel = () => {
this.close();
};
this.root.render(
<ModelEditModalContent
model={this.model}
isEmbeddingModel={this.isEmbeddingModel}
onUpdate={handleUpdate}
onCancel={handleCancel}
/>
);
}
onClose() {
this.root.unmount();
}
}

View file

@ -4,19 +4,48 @@ import { BUILTIN_CHAT_MODELS, BUILTIN_EMBEDDING_MODELS } from "@/constants";
import EmbeddingManager from "@/LLMProviders/embeddingManager";
import ProjectManager from "@/LLMProviders/projectManager";
import { logError } from "@/logger";
import { setSettings, updateSetting, useSettingsValue } from "@/settings/model";
import { CopilotSettings, setSettings, updateSetting, useSettingsValue } from "@/settings/model";
import { ModelAddDialog } from "@/settings/v2/components/ModelAddDialog";
import { ModelEditDialog } from "@/settings/v2/components/ModelEditDialog";
import { ModelEditModal } from "@/settings/v2/components/ModelEditDialog";
import { ModelTable } from "@/settings/v2/components/ModelTable";
import { omit } from "@/utils";
import { Notice } from "obsidian";
import React, { useState } from "react";
export const ModelSettings: React.FC = () => {
const settings = useSettingsValue();
const [editingModel, setEditingModel] = useState<CustomModel | null>(null);
const [showAddDialog, setShowAddDialog] = useState(false);
const [showAddEmbeddingDialog, setShowAddEmbeddingDialog] = useState(false);
const onCopyModel = (model: CustomModel, isEmbeddingModel: boolean = false) => {
const newModel: CustomModel = {
...omit(model, [
"isBuiltIn",
"core",
"projectEnabled",
"plusExclusive",
"believerExclusive",
"capabilities",
"displayName",
"dimensions",
]),
name: `${model.name} (copy)`,
};
const settingField: keyof CopilotSettings = isEmbeddingModel
? "activeEmbeddingModels"
: "activeModels";
updateSetting(settingField, [...settings[settingField], newModel]);
};
const handleModelReorder = (newModels: CustomModel[], isEmbeddingModel: boolean = false) => {
const settingField: keyof CopilotSettings = isEmbeddingModel
? "activeEmbeddingModels"
: "activeModels";
updateSetting(settingField, newModels);
};
const onDeleteModel = (modelKey: string) => {
const [modelName, provider] = modelKey.split("|");
const updatedActiveModels = settings.activeModels.filter(
@ -37,14 +66,22 @@ export const ModelSettings: React.FC = () => {
});
};
const handleModelUpdate = (originalModel: CustomModel, updatedModel: CustomModel) => {
const modelIndex = settings.activeModels.findIndex(
const handleModelUpdate = (
isEmbeddingModel: boolean,
originalModel: CustomModel,
updatedModel: CustomModel
) => {
const settingField: keyof CopilotSettings = isEmbeddingModel
? "activeEmbeddingModels"
: "activeModels";
const modelIndex = settings[settingField].findIndex(
(m) => m.name === originalModel.name && m.provider === originalModel.provider
);
if (modelIndex !== -1) {
const updatedModels = [...settings.activeModels];
const updatedModels = [...settings[settingField]];
updatedModels[modelIndex] = updatedModel;
updateSetting("activeModels", updatedModels);
updateSetting(settingField, updatedModels);
} else {
new Notice("Could not find model to update");
logError("Could not find model to update:", originalModel);
@ -59,10 +96,6 @@ export const ModelSettings: React.FC = () => {
updateSetting("activeModels", updatedModels);
};
const handleModelReorder = (newModels: CustomModel[]) => {
updateSetting("activeModels", newModels);
};
const onDeleteEmbeddingModel = (modelKey: string) => {
const [modelName, provider] = modelKey.split("|");
const updatedModels = settings.activeEmbeddingModels.filter(
@ -78,10 +111,6 @@ export const ModelSettings: React.FC = () => {
updateSetting("activeEmbeddingModels", updatedModels);
};
const handleEmbeddingModelReorder = (newModels: CustomModel[]) => {
updateSetting("activeEmbeddingModels", newModels);
};
const handleRefreshChatModels = () => {
// Get all custom models (non-built-in models)
const customModels = settings.activeModels.filter((model) => !model.isBuiltIn);
@ -106,29 +135,27 @@ export const ModelSettings: React.FC = () => {
new Notice("Embedding models refreshed successfully");
};
const handleEditModel = (model: CustomModel, isEmbeddingModel: boolean = false) => {
const modal = new ModelEditModal(app, model, isEmbeddingModel, handleModelUpdate);
modal.open();
};
return (
<div className="tw-space-y-4">
<section>
<div className="tw-mb-3 tw-text-xl tw-font-bold">Chat Models</div>
<ModelTable
models={settings.activeModels}
onEdit={setEditingModel}
onEdit={(model) => handleEditModel(model)}
onCopy={(model) => onCopyModel(model)}
onDelete={onDeleteModel}
onAdd={() => setShowAddDialog(true)}
onUpdateModel={handleTableUpdate}
onReorderModels={handleModelReorder}
onReorderModels={(newModels) => handleModelReorder(newModels)}
onRefresh={handleRefreshChatModels}
title="Chat Model"
/>
{/* model edit dialog*/}
<ModelEditDialog
open={!!editingModel}
onOpenChange={(open) => !open && setEditingModel(null)}
model={editingModel}
onUpdate={handleModelUpdate}
/>
{/* model add dialog */}
<ModelAddDialog
open={showAddDialog}
@ -143,38 +170,6 @@ export const ModelSettings: React.FC = () => {
/>
<div className="tw-space-y-4">
<SettingItem
type="slider"
title="Temperature"
description="Default is 0.1. Higher values will result in more creativeness, but also more mistakes. Set to 0 for no randomness."
value={settings.temperature}
onChange={(value) => updateSetting("temperature", value)}
min={0}
max={2}
step={0.05}
/>
<SettingItem
type="slider"
title="Token limit"
description={
<>
<p>
The maximum number of <em>output tokens</em> to generate. Default is 6000.
</p>
<em>
This number plus the length of your prompt (input tokens) must be smaller than the
context window of the model.
</em>
</>
}
value={settings.maxTokens}
onChange={(value) => updateSetting("maxTokens", value)}
min={0}
max={65000}
step={100}
/>
<SettingItem
type="slider"
title="Conversation turns in context"
@ -192,10 +187,12 @@ export const ModelSettings: React.FC = () => {
<div className="tw-mb-3 tw-text-xl tw-font-bold">Embedding Models</div>
<ModelTable
models={settings.activeEmbeddingModels}
onEdit={(model) => handleEditModel(model, true)}
onDelete={onDeleteEmbeddingModel}
onCopy={(model) => onCopyModel(model, true)}
onAdd={() => setShowAddEmbeddingDialog(true)}
onUpdateModel={handleEmbeddingModelUpdate}
onReorderModels={handleEmbeddingModelReorder}
onReorderModels={(newModels) => handleModelReorder(newModels, true)}
onRefresh={handleRefreshEmbeddingModels}
title="Embedding Model"
/>

View file

@ -36,17 +36,27 @@ import { CSS } from "@dnd-kit/utilities";
import {
ChevronDown,
ChevronRight,
Copy,
Eye,
Globe,
GripVertical,
Lightbulb,
LucideProps,
MoreVertical,
Pencil,
PencilLine,
Plus,
RefreshCw,
Trash2,
} from "lucide-react";
import React, { ForwardRefExoticComponent, RefAttributes } from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useContainerContext } from "@/settings/v2/components/ContainerContext";
const CAPABILITY_ICONS: Record<
ModelCapability,
@ -110,27 +120,25 @@ const renderCapabilities = (model: CustomModel) => {
);
};
interface ModelTableProps {
models: CustomModel[];
onEdit?: (model: CustomModel) => void;
onDelete: (modelKey: string) => void;
onAdd: () => void;
onUpdateModel: (model: CustomModel) => void;
onReorderModels?: (newModels: CustomModel[]) => void;
onRefresh?: () => void;
title: string;
}
interface ModelCardProps {
model: CustomModel;
onEdit?: () => void;
onDelete?: () => void;
onEdit?: (model: CustomModel) => void;
onCopy?: (model: CustomModel) => void;
onDelete: (modelKey: string) => void;
onUpdateModel: (model: CustomModel) => void;
id: string;
}
const ModelCard: React.FC<ModelCardProps> = ({ model, onEdit, onDelete, onUpdateModel, id }) => {
const ModelCard: React.FC<ModelCardProps> = ({
model,
onEdit,
onCopy,
onDelete,
onUpdateModel,
id,
}) => {
const [isExpanded, setIsExpanded] = React.useState(false);
const container = useContainerContext();
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id,
@ -205,24 +213,57 @@ const ModelCard: React.FC<ModelCardProps> = ({ model, onEdit, onDelete, onUpdate
size="icon"
onClick={(e) => {
e.stopPropagation();
onEdit();
onEdit(model);
}}
>
<Pencil className="tw-size-4" />
</Button>
)}
{onDelete && !model.core && (
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
>
<Trash2 className="tw-size-4" />
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="tw-size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" container={container}>
{onEdit && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEdit(model);
}}
>
<PencilLine className="tw-mr-2 tw-size-4" />
Edit
</DropdownMenuItem>
)}
{onCopy && !model.core && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onCopy(model);
}}
>
<Copy className="tw-mr-2 tw-size-4" />
Copy
</DropdownMenuItem>
)}
{!model.core && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDelete(getModelKeyFromModel(model));
}}
className="tw-text-error"
>
<Trash2 className="tw-mr-2 tw-size-4" />
Delete
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardHeader>
@ -261,23 +302,27 @@ const ModelCard: React.FC<ModelCardProps> = ({ model, onEdit, onDelete, onUpdate
);
};
const SortableTableRow: React.FC<{
const DesktopSortableTableRow: React.FC<{
model: CustomModel;
onEdit?: (model: CustomModel) => void;
onCopy?: (model: CustomModel) => void;
onDelete: (modelKey: string) => void;
onUpdateModel: (model: CustomModel) => void;
isEmbeddingModel: boolean;
}> = ({ model, onEdit, onDelete, onUpdateModel, isEmbeddingModel }) => {
}> = ({ model, onEdit, onCopy, onDelete, onUpdateModel, isEmbeddingModel }) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: getModelKeyFromModel(model),
disabled: model.core,
});
const container = useContainerContext();
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const showDropdownMenu = onEdit || !model.core || (onCopy && !model.core);
return (
<TableRow
ref={setNodeRef}
@ -334,15 +379,40 @@ const SortableTableRow: React.FC<{
<Pencil className="tw-size-4" />
</Button>
)}
{!model.core && (
<Button
variant="ghost"
size="icon"
onClick={() => onDelete(getModelKeyFromModel(model))}
className="tw-shadow-sm tw-transition-shadow hover:tw-shadow-md"
>
<Trash2 className="tw-size-4" />
</Button>
{showDropdownMenu && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="tw-size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" container={container}>
{onEdit && (
<DropdownMenuItem onClick={() => onEdit(model)}>
<PencilLine className="tw-mr-2 tw-size-4" />
Edit
</DropdownMenuItem>
)}
{onCopy && !model.core && (
<DropdownMenuItem onClick={() => onCopy(model)}>
<Copy className="tw-mr-2 tw-size-4" />
Copy
</DropdownMenuItem>
)}
{!model.core && (
<DropdownMenuItem
onClick={() => onDelete(getModelKeyFromModel(model))}
className="tw-text-error"
>
<Trash2 className="tw-mr-2 tw-size-4" />
Delete
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</TableCell>
@ -350,9 +420,22 @@ const SortableTableRow: React.FC<{
);
};
interface ModelTableProps {
models: CustomModel[];
onEdit?: (model: CustomModel) => void;
onCopy?: (model: CustomModel) => void;
onDelete: (modelKey: string) => void;
onAdd: () => void;
onUpdateModel: (model: CustomModel) => void;
onReorderModels?: (newModels: CustomModel[]) => void;
onRefresh?: () => void;
title: string;
}
export const ModelTable: React.FC<ModelTableProps> = ({
models,
onEdit,
onCopy,
onDelete,
onAdd,
onUpdateModel,
@ -457,8 +540,9 @@ export const ModelTable: React.FC<ModelTableProps> = ({
key={getModelKeyFromModel(model)}
id={getModelKeyFromModel(model)}
model={model}
onEdit={onEdit ? () => onEdit(model) : undefined}
onDelete={() => onDelete(getModelKeyFromModel(model))}
onEdit={onEdit}
onCopy={onCopy}
onDelete={onDelete}
onUpdateModel={onUpdateModel}
/>
))}
@ -497,10 +581,11 @@ export const ModelTable: React.FC<ModelTableProps> = ({
strategy={verticalListSortingStrategy}
>
{models.map((model) => (
<SortableTableRow
<DesktopSortableTableRow
key={getModelKeyFromModel(model)}
model={model}
onEdit={onEdit ? () => onEdit(model) : undefined}
onEdit={onEdit}
onCopy={onCopy}
onDelete={onDelete}
onUpdateModel={onUpdateModel}
isEmbeddingModel={isEmbeddingModel}

View file

@ -12,7 +12,7 @@ describe("projectUtils", () => {
projectModelKey: "gpt-3.5-turbo",
modelConfigs: {
temperature: 0.7,
maxTokens: 1000,
maxTokens: 6000,
},
contextSource: {
inclusions: "src/**/*.tsx",

View file

@ -487,12 +487,6 @@ export async function safeFetch(url: string, options: RequestInit = {}): Promise
// Remove content-length if it exists
delete (headers as Record<string, string>)["content-length"];
if (typeof options.body === "string") {
const newBody = JSON.parse(options.body ?? {});
delete newBody["frequency_penalty"];
options.body = JSON.stringify(newBody);
}
logInfo("==== safeFetch method request ====");
const method = options.method?.toUpperCase() || "POST";