SettingGeminiModel.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useEffect, useState, useRef } from 'react';
  16. import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
  17. import {
  18. compareObjects,
  19. API,
  20. showError,
  21. showSuccess,
  22. showWarning,
  23. verifyJSON,
  24. } from '../../../helpers';
  25. import { useTranslation } from 'react-i18next';
  26. import Text from '@douyinfe/semi-ui/lib/es/typography/text';
  27. const GEMINI_SETTING_EXAMPLE = {
  28. default: 'OFF',
  29. HARM_CATEGORY_CIVIC_INTEGRITY: 'BLOCK_NONE',
  30. };
  31. const GEMINI_VERSION_EXAMPLE = {
  32. default: 'v1beta',
  33. };
  34. const DEFAULT_GEMINI_INPUTS = {
  35. 'gemini.safety_settings': '',
  36. 'gemini.version_settings': '',
  37. 'gemini.supported_imagine_models': '',
  38. 'gemini.thinking_adapter_enabled': false,
  39. 'gemini.thinking_adapter_budget_tokens_percentage': 0.6,
  40. 'gemini.function_call_thought_signature_enabled': true,
  41. 'gemini.remove_function_response_id_enabled': true,
  42. };
  43. export default function SettingGeminiModel(props) {
  44. const { t } = useTranslation();
  45. const [loading, setLoading] = useState(false);
  46. const [inputs, setInputs] = useState(DEFAULT_GEMINI_INPUTS);
  47. const refForm = useRef();
  48. const [inputsRow, setInputsRow] = useState(DEFAULT_GEMINI_INPUTS);
  49. async function onSubmit() {
  50. await refForm.current
  51. .validate()
  52. .then(() => {
  53. const updateArray = compareObjects(inputs, inputsRow);
  54. if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
  55. const requestQueue = updateArray.map((item) => {
  56. let value = String(inputs[item.key]);
  57. return API.put('/api/option/', {
  58. key: item.key,
  59. value,
  60. });
  61. });
  62. setLoading(true);
  63. Promise.all(requestQueue)
  64. .then((res) => {
  65. if (requestQueue.length === 1) {
  66. if (res.includes(undefined)) return;
  67. } else if (requestQueue.length > 1) {
  68. if (res.includes(undefined))
  69. return showError(t('部分保存失败,请重试'));
  70. }
  71. showSuccess(t('保存成功'));
  72. props.refresh();
  73. })
  74. .catch(() => {
  75. showError(t('保存失败,请重试'));
  76. })
  77. .finally(() => {
  78. setLoading(false);
  79. });
  80. })
  81. .catch((error) => {
  82. console.error('Validation failed:', error);
  83. showError(t('请检查输入'));
  84. });
  85. }
  86. useEffect(() => {
  87. const currentInputs = { ...DEFAULT_GEMINI_INPUTS };
  88. for (let key in props.options) {
  89. if (Object.prototype.hasOwnProperty.call(DEFAULT_GEMINI_INPUTS, key)) {
  90. currentInputs[key] = props.options[key];
  91. }
  92. }
  93. setInputs(currentInputs);
  94. setInputsRow(structuredClone(currentInputs));
  95. refForm.current.setValues(currentInputs);
  96. }, [props.options]);
  97. return (
  98. <>
  99. <Spin spinning={loading}>
  100. <Form
  101. values={inputs}
  102. getFormApi={(formAPI) => (refForm.current = formAPI)}
  103. style={{ marginBottom: 15 }}
  104. >
  105. <Form.Section text={t('Gemini设置')}>
  106. <Row>
  107. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  108. <Form.TextArea
  109. label={t('Gemini安全设置')}
  110. placeholder={
  111. t('为一个 JSON 文本,例如:') +
  112. '\n' +
  113. JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)
  114. }
  115. field={'gemini.safety_settings'}
  116. extraText={t(
  117. 'default为默认设置,可单独设置每个分类的安全等级',
  118. )}
  119. autosize={{ minRows: 6, maxRows: 12 }}
  120. trigger='blur'
  121. stopValidateWithError
  122. rules={[
  123. {
  124. validator: (rule, value) => verifyJSON(value),
  125. message: t('不是合法的 JSON 字符串'),
  126. },
  127. ]}
  128. onChange={(value) =>
  129. setInputs({ ...inputs, 'gemini.safety_settings': value })
  130. }
  131. />
  132. </Col>
  133. </Row>
  134. <Row>
  135. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  136. <Form.TextArea
  137. label={t('Gemini版本设置')}
  138. placeholder={
  139. t('为一个 JSON 文本,例如:') +
  140. '\n' +
  141. JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)
  142. }
  143. field={'gemini.version_settings'}
  144. extraText={t('default为默认设置,可单独设置每个模型的版本')}
  145. autosize={{ minRows: 6, maxRows: 12 }}
  146. trigger='blur'
  147. stopValidateWithError
  148. rules={[
  149. {
  150. validator: (rule, value) => verifyJSON(value),
  151. message: t('不是合法的 JSON 字符串'),
  152. },
  153. ]}
  154. onChange={(value) =>
  155. setInputs({ ...inputs, 'gemini.version_settings': value })
  156. }
  157. />
  158. </Col>
  159. </Row>
  160. <Row>
  161. <Col span={16}>
  162. <Form.Switch
  163. label={t('启用FunctionCall思维签名填充')}
  164. field={'gemini.function_call_thought_signature_enabled'}
  165. extraText={t(
  166. '仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature',
  167. )}
  168. onChange={(value) =>
  169. setInputs({
  170. ...inputs,
  171. 'gemini.function_call_thought_signature_enabled': value,
  172. })
  173. }
  174. />
  175. </Col>
  176. </Row>
  177. <Row>
  178. <Col span={16}>
  179. <Form.Switch
  180. label={t('移除 functionResponse.id 字段')}
  181. field={'gemini.remove_function_response_id_enabled'}
  182. extraText={t(
  183. 'Vertex AI 不支持 functionResponse.id 字段,开启后将自动移除该字段',
  184. )}
  185. onChange={(value) =>
  186. setInputs({
  187. ...inputs,
  188. 'gemini.remove_function_response_id_enabled': value,
  189. })
  190. }
  191. />
  192. </Col>
  193. </Row>
  194. <Row>
  195. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  196. <Form.TextArea
  197. field={'gemini.supported_imagine_models'}
  198. label={t('支持的图像模型')}
  199. placeholder={
  200. t('例如:') +
  201. '\n' +
  202. JSON.stringify(
  203. ['gemini-2.0-flash-exp-image-generation'],
  204. null,
  205. 2,
  206. )
  207. }
  208. onChange={(value) =>
  209. setInputs({
  210. ...inputs,
  211. 'gemini.supported_imagine_models': value,
  212. })
  213. }
  214. trigger='blur'
  215. stopValidateWithError
  216. rules={[
  217. {
  218. validator: (rule, value) => verifyJSON(value),
  219. message: t('不是合法的 JSON 字符串'),
  220. },
  221. ]}
  222. />
  223. </Col>
  224. </Row>
  225. </Form.Section>
  226. <Form.Section text={t('Gemini思考适配设置')}>
  227. <Row>
  228. <Col span={16}>
  229. <Text>
  230. {t(
  231. '和Claude不同,默认情况下Gemini的思考模型会自动决定要不要思考,就算不开启适配模型也可以正常使用,' +
  232. '如果您需要计费,推荐设置无后缀模型价格按思考价格设置。' +
  233. '支持使用 gemini-2.5-pro-preview-06-05-thinking-128 格式来精确传递思考预算。',
  234. )}
  235. </Text>
  236. </Col>
  237. </Row>
  238. <Row>
  239. <Col span={16}>
  240. <Form.Switch
  241. label={t('启用Gemini思考后缀适配')}
  242. field={'gemini.thinking_adapter_enabled'}
  243. extraText={t(
  244. '适配 -thinking、-thinking-预算数字 和 -nothinking 后缀',
  245. )}
  246. onChange={(value) =>
  247. setInputs({
  248. ...inputs,
  249. 'gemini.thinking_adapter_enabled': value,
  250. })
  251. }
  252. />
  253. </Col>
  254. </Row>
  255. <Row>
  256. <Col span={16}>
  257. <Text>
  258. {t(
  259. 'Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比',
  260. )}
  261. </Text>
  262. </Col>
  263. </Row>
  264. <Row>
  265. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  266. <Form.InputNumber
  267. label={t('思考预算占比')}
  268. field={'gemini.thinking_adapter_budget_tokens_percentage'}
  269. initValue={''}
  270. extraText={t('0.002-1之间的小数')}
  271. min={0.002}
  272. max={1}
  273. onChange={(value) =>
  274. setInputs({
  275. ...inputs,
  276. 'gemini.thinking_adapter_budget_tokens_percentage': value,
  277. })
  278. }
  279. />
  280. </Col>
  281. </Row>
  282. </Form.Section>
  283. <Row>
  284. <Button size='default' onClick={onSubmit}>
  285. {t('保存')}
  286. </Button>
  287. </Row>
  288. </Form>
  289. </Spin>
  290. </>
  291. );
  292. }