diff --git a/app/client/api.ts b/app/client/api.ts index f60b0e2ad71..0f972c01d0d 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -11,6 +11,7 @@ import { useAccessStore, useChatStore, } from "../store"; +import type { ModelConfig } from "../store/config"; import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai"; import { GeminiProApi } from "./platforms/google"; import { ClaudeApi } from "./platforms/anthropic"; @@ -397,3 +398,78 @@ export function getClientApi(provider: ServiceProvider): ClientApi { return new ClientApi(ModelProvider.GPT); } } + +// Core fields that must not be overwritten by extraParams +const PROTECTED_PAYLOAD_KEYS = new Set([ + "messages", + "model", + "stream", + "temperature", + "top_p", + "presence_penalty", + "frequency_penalty", +]); + +/** + * Apply model-config extras to a request payload (called after requestPayload + * has been constructed in each provider): + * 1. Remove parameters flagged as "do not send" so the provider defaults + * are used instead. + * 2. Parse modelConfig.extraParams (a JSON string) and merge it into the + * payload. + * 3. Protect core fields from being overwritten by extraParams. + * + * Note: temperature/top_p/presence_penalty/frequency_penalty are in the + * protected set because they can be removed by the disable flags above; + * adding them back via extraParams would defeat that intent. + */ +export function applyModelConfigExtras( + modelConfig: ModelConfig, + payload: Record, +): Record { + if (!payload || typeof payload !== "object") { + return payload; + } + + // 1. Remove parameters the user chose not to send + if (modelConfig.disableTemperature) { + delete payload.temperature; + } + if (modelConfig.disableTopP) { + delete payload.top_p; + } + if (modelConfig.disablePresencePenalty) { + delete payload.presence_penalty; + } + if (modelConfig.disableFrequencyPenalty) { + delete payload.frequency_penalty; + } + + // 2. Merge custom request parameters + const extraParamsStr = modelConfig.extraParams; + if (extraParamsStr && extraParamsStr.trim().length > 0) { + try { + const extra = JSON.parse(extraParamsStr); + if (extra && typeof extra === "object" && !Array.isArray(extra)) { + Object.keys(extra).forEach((key) => { + if (!PROTECTED_PAYLOAD_KEYS.has(key)) { + payload[key] = extra[key]; + } else { + console.warn( + `[applyModelConfigExtras] skip protected key "${key}"`, + ); + } + }); + } else { + console.warn( + "[applyModelConfigExtras] extraParams must be a JSON object, got:", + extra, + ); + } + } catch (e) { + console.warn("[applyModelConfigExtras] invalid extraParams JSON:", e); + } + } + + return payload; +} diff --git a/app/client/platforms/ai302.ts b/app/client/platforms/ai302.ts index 7bd18f96d4c..83bad21ce87 100644 --- a/app/client/platforms/ai302.ts +++ b/app/client/platforms/ai302.ts @@ -20,6 +20,7 @@ import { LLMApi, LLMModel, SpeechOptions, + applyModelConfigExtras, } from "../api"; import { getClientConfig } from "@/app/config/client"; import { @@ -117,6 +118,8 @@ export class Ai302Api implements LLMApi { // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; + applyModelConfigExtras(modelConfig, requestPayload); + console.log("[Request] openai payload: ", requestPayload); const shouldStream = !!options.config.stream; diff --git a/app/client/platforms/alibaba.ts b/app/client/platforms/alibaba.ts index 4875e5c02d9..b844af90230 100644 --- a/app/client/platforms/alibaba.ts +++ b/app/client/platforms/alibaba.ts @@ -19,6 +19,7 @@ import { SpeechOptions, MultimodalContent, MultimodalContentForAlibaba, + applyModelConfigExtras, } from "../api"; import { getClientConfig } from "@/app/config/client"; import { @@ -132,6 +133,18 @@ export class QwenApi implements LLMApi { }, }; + // Alibaba nests temperature/top_p inside parameters, so handle the + // disable flags specifically for those nested fields + if (modelConfig.disableTemperature) { + delete (requestPayload.parameters as any).temperature; + } + if (modelConfig.disableTopP) { + delete (requestPayload.parameters as any).top_p; + } + // Merge custom extraParams at the top level; top-level disable flags + // are no-ops here because Alibaba does not use those top-level names + applyModelConfigExtras(modelConfig, requestPayload as any); + const controller = new AbortController(); options.onController?.(controller); diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index 2377470904f..1cece531323 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -1,5 +1,5 @@ import { Anthropic, ApiPath } from "@/app/constant"; -import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api"; +import { ChatOptions, getHeaders, LLMApi, SpeechOptions, applyModelConfigExtras } from "../api"; import { useAccessStore, useAppConfig, @@ -191,6 +191,11 @@ export class ClaudeApi implements LLMApi { top_k: 5, }; + // Apply disable flags (temperature/top_p) + merge custom extraParams + // presence_penalty / frequency_penalty are not supported by Anthropic, + // so those disable flags are effectively no-ops here + applyModelConfigExtras(modelConfig, requestBody as any); + const path = this.path(Anthropic.ChatPath); const controller = new AbortController(); diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index dc990db4103..31a2a247825 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -10,6 +10,7 @@ import { LLMModel, MultimodalContent, SpeechOptions, + applyModelConfigExtras, } from "../api"; import Locale from "../../locales"; import { @@ -117,6 +118,8 @@ export class ErnieApi implements LLMApi { top_p: modelConfig.top_p, }; + applyModelConfigExtras(modelConfig, requestPayload); + console.log("[Request] Baidu payload: ", requestPayload); const controller = new AbortController(); diff --git a/app/client/platforms/bytedance.ts b/app/client/platforms/bytedance.ts index f9524cba28d..569ae930c70 100644 --- a/app/client/platforms/bytedance.ts +++ b/app/client/platforms/bytedance.ts @@ -15,6 +15,7 @@ import { LLMModel, MultimodalContent, SpeechOptions, + applyModelConfigExtras, } from "../api"; import { streamWithThink } from "@/app/utils/chat"; @@ -113,6 +114,8 @@ export class DoubaoApi implements LLMApi { top_p: modelConfig.top_p, }; + applyModelConfigExtras(modelConfig, requestPayload); + const controller = new AbortController(); options.onController?.(controller); diff --git a/app/client/platforms/deepseek.ts b/app/client/platforms/deepseek.ts index 1b38b40cc64..b65eee1560d 100644 --- a/app/client/platforms/deepseek.ts +++ b/app/client/platforms/deepseek.ts @@ -15,6 +15,7 @@ import { LLMApi, LLMModel, SpeechOptions, + applyModelConfigExtras, } from "../api"; import { getClientConfig } from "@/app/config/client"; import { @@ -115,6 +116,8 @@ export class DeepSeekApi implements LLMApi { // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; + applyModelConfigExtras(modelConfig, requestPayload); + console.log("[Request] openai payload: ", requestPayload); const shouldStream = !!options.config.stream; diff --git a/app/client/platforms/glm.ts b/app/client/platforms/glm.ts index 98b10277de9..2cd00ddaf01 100644 --- a/app/client/platforms/glm.ts +++ b/app/client/platforms/glm.ts @@ -14,6 +14,7 @@ import { LLMApi, LLMModel, SpeechOptions, + applyModelConfigExtras, } from "../api"; import { getClientConfig } from "@/app/config/client"; import { @@ -175,6 +176,11 @@ export class ChatGLMApi implements LLMApi { const requestPayload = this.createPayload(messages, modelConfig, options); const path = this.path(this.getModelPath(modelType)); + // Apply disable flags + merge custom extraParams (chat only; skip for image/video) + if (modelType !== "image" && modelType !== "video") { + applyModelConfigExtras(modelConfig, requestPayload as any); + } + console.log(`[Request] glm ${modelType} payload: `, requestPayload); const controller = new AbortController(); diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 654f0e3e4c7..f0d45b82c28 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -6,6 +6,7 @@ import { LLMModel, LLMUsage, SpeechOptions, + applyModelConfigExtras, } from "../api"; import { useAccessStore, @@ -181,6 +182,19 @@ export class GeminiProApi implements LLMApi { ], }; + // Google nests temperature/topP inside generationConfig, so handle the + // disable flags specifically for those nested fields + if (modelConfig.disableTemperature) { + delete requestPayload.generationConfig.temperature; + } + if (modelConfig.disableTopP) { + delete requestPayload.generationConfig.topP; + } + // Merge custom extraParams at the top level; top-level disable flags + // (temperature/top_p/presence_penalty/frequency_penalty) are no-ops here + // because Google does not use those top-level field names + applyModelConfigExtras(modelConfig, requestPayload); + let shouldStream = !!options.config.stream; const controller = new AbortController(); options.onController?.(controller); diff --git a/app/client/platforms/iflytek.ts b/app/client/platforms/iflytek.ts index cfc37b3b256..6048a996e51 100644 --- a/app/client/platforms/iflytek.ts +++ b/app/client/platforms/iflytek.ts @@ -13,6 +13,7 @@ import { LLMApi, LLMModel, SpeechOptions, + applyModelConfigExtras, } from "../api"; import Locale from "../../locales"; import { @@ -92,6 +93,8 @@ export class SparkApi implements LLMApi { // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; + applyModelConfigExtras(modelConfig, requestPayload); + console.log("[Request] Spark payload: ", requestPayload); const shouldStream = !!options.config.stream; diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts index b6812c0d766..a292d9666d5 100644 --- a/app/client/platforms/moonshot.ts +++ b/app/client/platforms/moonshot.ts @@ -20,6 +20,7 @@ import { LLMApi, LLMModel, SpeechOptions, + applyModelConfigExtras, } from "../api"; import { getClientConfig } from "@/app/config/client"; import { getMessageTextContent } from "@/app/utils"; @@ -92,6 +93,8 @@ export class MoonshotApi implements LLMApi { // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; + applyModelConfigExtras(modelConfig, requestPayload); + console.log("[Request] openai payload: ", requestPayload); const shouldStream = !!options.config.stream; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index cfbff99e978..bc0bfd41512 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -34,6 +34,7 @@ import { LLMUsage, MultimodalContent, SpeechOptions, + applyModelConfigExtras, } from "../api"; import Locale from "../../locales"; import { getClientConfig } from "@/app/config/client"; @@ -263,6 +264,9 @@ export class ChatGPTApi implements LLMApi { if (visionModel && !isO1OrO3 && ! isGpt5) { requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); } + + // Apply disable flags + merge custom extraParams (chat only; skip for Dalle3) + requestPayload = applyModelConfigExtras(modelConfig, requestPayload as any) as any; } console.log("[Request] openai payload: ", requestPayload); diff --git a/app/client/platforms/siliconflow.ts b/app/client/platforms/siliconflow.ts index 34f0844c328..a09c3c01166 100644 --- a/app/client/platforms/siliconflow.ts +++ b/app/client/platforms/siliconflow.ts @@ -20,6 +20,7 @@ import { LLMApi, LLMModel, SpeechOptions, + applyModelConfigExtras, } from "../api"; import { getClientConfig } from "@/app/config/client"; import { @@ -117,6 +118,8 @@ export class SiliconflowApi implements LLMApi { // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; + applyModelConfigExtras(modelConfig, requestPayload); + console.log("[Request] openai payload: ", requestPayload); const shouldStream = !!options.config.stream; diff --git a/app/client/platforms/tencent.ts b/app/client/platforms/tencent.ts index 8adeb1b3ea7..d563ce50a0f 100644 --- a/app/client/platforms/tencent.ts +++ b/app/client/platforms/tencent.ts @@ -9,6 +9,7 @@ import { LLMModel, MultimodalContent, SpeechOptions, + applyModelConfigExtras, } from "../api"; import Locale from "../../locales"; import { @@ -113,13 +114,24 @@ export class HunyuanApi implements LLMApi { }, }; - const requestPayload: RequestPayload = capitalizeKeys({ + // Tencent payload keys get uppercased by capitalizeKeys, so construct the + // raw object conditionally based on disable flags before capitalizing + const basePayload: Record = { model: modelConfig.model, messages, - temperature: modelConfig.temperature, - top_p: modelConfig.top_p, stream: options.config.stream, - }); + }; + if (!modelConfig.disableTemperature) { + basePayload.temperature = modelConfig.temperature; + } + if (!modelConfig.disableTopP) { + basePayload.top_p = modelConfig.top_p; + } + const requestPayload: RequestPayload = capitalizeKeys(basePayload); + + // Merge custom extraParams (top-level). Top-level disable flags are no-ops + // here because all keys have been capitalized already + applyModelConfigExtras(modelConfig, requestPayload as any); console.log("[Request] Tencent payload: ", requestPayload); diff --git a/app/client/platforms/xai.ts b/app/client/platforms/xai.ts index 830ad4778ac..76814a84cff 100644 --- a/app/client/platforms/xai.ts +++ b/app/client/platforms/xai.ts @@ -15,6 +15,7 @@ import { LLMApi, LLMModel, SpeechOptions, + applyModelConfigExtras, } from "../api"; import { getClientConfig } from "@/app/config/client"; import { getTimeoutMSByModel } from "@/app/utils"; @@ -86,6 +87,8 @@ export class XAIApi implements LLMApi { top_p: modelConfig.top_p, }; + applyModelConfigExtras(modelConfig, requestPayload); + console.log("[Request] xai payload: ", requestPayload); const shouldStream = !!options.config.stream; diff --git a/app/components/input-range.tsx b/app/components/input-range.tsx index 3c6b4982579..79fe6931d30 100644 --- a/app/components/input-range.tsx +++ b/app/components/input-range.tsx @@ -11,6 +11,7 @@ interface InputRangeProps { max: string; step: string; aria: string; + disabled?: boolean; } export function InputRange({ @@ -22,6 +23,7 @@ export function InputRange({ max, step, aria, + disabled, }: InputRangeProps) { return (
@@ -35,6 +37,7 @@ export function InputRange({ max={max} step={step} onChange={onChange} + disabled={disabled} >
); diff --git a/app/components/model-config.module.scss b/app/components/model-config.module.scss index 40ba03f8657..9992a0bcd32 100644 --- a/app/components/model-config.module.scss +++ b/app/components/model-config.module.scss @@ -5,3 +5,43 @@ white-space: normal; } } + +.param-with-toggle { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; +} + +.param-disable-label { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--black); + opacity: 0.7; + cursor: pointer; + user-select: none; + + input[type="checkbox"] { + cursor: pointer; + } +} + +.extra-params-input { + width: 100%; + min-height: 80px; + resize: vertical; + font-family: monospace; + font-size: 12px; + padding: 8px; + border-radius: 8px; + border: 1px solid var(--border-in-light); + background: var(--white); + color: var(--black); + box-sizing: border-box; + + &::placeholder { + color: var(--gray); + } +} diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index e845bfeac7a..d8eb64db70b 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -53,41 +53,72 @@ export function ModelConfigList(props: { title={Locale.Settings.Temperature.Title} subTitle={Locale.Settings.Temperature.SubTitle} > - { - props.updateConfig( - (config) => - (config.temperature = ModalConfigValidator.temperature( - e.currentTarget.valueAsNumber, - )), - ); - }} - > +
+ { + props.updateConfig( + (config) => + (config.temperature = ModalConfigValidator.temperature( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + +
- { - props.updateConfig( - (config) => - (config.top_p = ModalConfigValidator.top_p( - e.currentTarget.valueAsNumber, - )), - ); - }} - > +
+ { + props.updateConfig( + (config) => + (config.top_p = ModalConfigValidator.top_p( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + +
- { - props.updateConfig( - (config) => - (config.presence_penalty = - ModalConfigValidator.presence_penalty( - e.currentTarget.valueAsNumber, - )), - ); - }} - > +
+ { + props.updateConfig( + (config) => + (config.presence_penalty = + ModalConfigValidator.presence_penalty( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + +
- { - props.updateConfig( - (config) => - (config.frequency_penalty = - ModalConfigValidator.frequency_penalty( - e.currentTarget.valueAsNumber, - )), - ); - }} - > +
+ { + props.updateConfig( + (config) => + (config.frequency_penalty = + ModalConfigValidator.frequency_penalty( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + +
+ +