Explorar el Código

feat(playground): enhance max_tokens handling and input sanitization

- Introduced `sanitizePlaygroundInputs` to normalize `max_tokens` input values.
- Updated `loadConfig` to utilize the new sanitization function.
- Replaced `Input` with `InputNumber` for `max_tokens` in `ParameterControl` for better user experience.
- Modified API payload building logic to handle `max_tokens` more robustly.
- Added tests for new helper functions in `playgroundMaxTokens.js` to ensure correct behavior.
HynoR hace 1 mes
padre
commit
4cd0e3651d

+ 13 - 7
web/src/components/playground/ParameterControl.jsx

@@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React from 'react';
-import { Input, Slider, Typography, Button, Tag } from '@douyinfe/semi-ui';
+import {
+  Input,
+  InputNumber,
+  Slider,
+  Typography,
+  Button,
+  Tag,
+} from '@douyinfe/semi-ui';
 import { useTranslation } from 'react-i18next';
 import {
   Hash,
@@ -241,15 +248,14 @@ const ParameterControl = ({
             disabled={disabled}
           />
         </div>
-        <Input
+        <InputNumber
           placeholder='MaxTokens'
           name='max_tokens'
-          required
-          autoComplete='new-password'
-          defaultValue={0}
           value={inputs.max_tokens}
-          onChange={(value) => onInputChange('max_tokens', value)}
-          className='!rounded-lg'
+          onNumberChange={(value) => onInputChange('max_tokens', value)}
+          min={0}
+          precision={0}
+          style={{ width: '100%' }}
           disabled={!parameterEnabled.max_tokens || disabled}
         />
       </div>

+ 3 - 2
web/src/components/playground/configStorage.js

@@ -21,6 +21,7 @@ import {
   STORAGE_KEYS,
   DEFAULT_CONFIG,
 } from '../../constants/playground.constants';
+import { sanitizePlaygroundInputs } from '../../helpers/playgroundMaxTokens';
 
 const MESSAGES_STORAGE_KEY = 'playground_messages';
 
@@ -67,10 +68,10 @@ export const loadConfig = () => {
       const parsedConfig = JSON.parse(savedConfig);
 
       const mergedConfig = {
-        inputs: {
+        inputs: sanitizePlaygroundInputs({
           ...DEFAULT_CONFIG.inputs,
           ...parsedConfig.inputs,
-        },
+        }),
         parameterEnabled: {
           ...DEFAULT_CONFIG.parameterEnabled,
           ...parsedConfig.parameterEnabled,

+ 12 - 1
web/src/helpers/api.js

@@ -150,7 +150,18 @@ export const buildApiPayload = (
     const value = inputs[param];
     const hasValue = value !== undefined && value !== null;
 
-    if (enabled && hasValue) {
+    if (!enabled) {
+      return;
+    }
+
+    if (param === 'max_tokens') {
+      if (typeof value === 'number') {
+        payload[param] = value;
+      }
+      return;
+    }
+
+    if (hasValue) {
       payload[param] = value;
     }
   });

+ 1 - 0
web/src/helpers/index.js

@@ -30,3 +30,4 @@ export * from './boolean';
 export * from './dashboard';
 export * from './passkey';
 export * from './statusCodeRules';
+export * from './playgroundMaxTokens';

+ 55 - 0
web/src/helpers/playgroundMaxTokens.js

@@ -0,0 +1,55 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+export const normalizeMaxTokensValue = (value) => {
+  if (typeof value === 'number') {
+    return Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
+  }
+
+  if (typeof value === 'string') {
+    const trimmed = value.trim();
+    if (trimmed === '') {
+      return null;
+    }
+
+    const parsed = Number(trimmed);
+    return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : null;
+  }
+
+  return null;
+};
+
+export const normalizePlaygroundInputValue = (name, value) => {
+  if (name === 'max_tokens') {
+    return normalizeMaxTokensValue(value);
+  }
+
+  return value;
+};
+
+export const sanitizePlaygroundInputs = (inputs) => {
+  if (!inputs) {
+    return inputs;
+  }
+
+  return {
+    ...inputs,
+    max_tokens: normalizeMaxTokensValue(inputs.max_tokens),
+  };
+};

+ 43 - 0
web/src/helpers/playgroundMaxTokens.test.mjs

@@ -0,0 +1,43 @@
+import assert from 'node:assert/strict';
+
+import {
+  normalizeMaxTokensValue,
+  normalizePlaygroundInputValue,
+  sanitizePlaygroundInputs,
+} from './playgroundMaxTokens.js';
+
+assert.equal(normalizeMaxTokensValue(8192), 8192);
+assert.equal(normalizeMaxTokensValue('8192'), 8192);
+assert.equal(normalizeMaxTokensValue(' 8192 '), 8192);
+assert.equal(normalizeMaxTokensValue(''), null);
+assert.equal(normalizeMaxTokensValue('abc'), null);
+assert.equal(normalizeMaxTokensValue(-1), null);
+assert.equal(normalizeMaxTokensValue(1.9), 1);
+
+assert.equal(normalizePlaygroundInputValue('max_tokens', '2048'), 2048);
+assert.equal(normalizePlaygroundInputValue('max_tokens', 'bad'), null);
+assert.equal(normalizePlaygroundInputValue('seed', 'bad'), 'bad');
+
+assert.deepEqual(
+  sanitizePlaygroundInputs({
+    model: 'gpt-4o',
+    max_tokens: '2048',
+  }),
+  {
+    model: 'gpt-4o',
+    max_tokens: 2048,
+  },
+);
+
+assert.deepEqual(
+  sanitizePlaygroundInputs({
+    model: 'gpt-4o',
+    max_tokens: 'bad',
+  }),
+  {
+    model: 'gpt-4o',
+    max_tokens: null,
+  },
+);
+
+console.log('playground max_tokens tests passed');

+ 12 - 3
web/src/hooks/playground/usePlaygroundState.js

@@ -32,7 +32,11 @@ import {
   loadMessages,
   saveMessages,
 } from '../../components/playground/configStorage';
-import { processIncompleteThinkTags } from '../../helpers';
+import {
+  processIncompleteThinkTags,
+  normalizePlaygroundInputValue,
+  sanitizePlaygroundInputs,
+} from '../../helpers';
 
 export const usePlaygroundState = () => {
   const { t } = useTranslation();
@@ -121,7 +125,10 @@ export const usePlaygroundState = () => {
 
   // 配置更新函数
   const handleInputChange = useCallback((name, value) => {
-    setInputs((prev) => ({ ...prev, [name]: value }));
+    setInputs((prev) => ({
+      ...prev,
+      [name]: normalizePlaygroundInputValue(name, value),
+    }));
   }, []);
 
   const handleParameterToggle = useCallback((paramName) => {
@@ -167,7 +174,9 @@ export const usePlaygroundState = () => {
   // 配置导入/重置
   const handleConfigImport = useCallback((importedConfig) => {
     if (importedConfig.inputs) {
-      setInputs((prev) => ({ ...prev, ...importedConfig.inputs }));
+      setInputs((prev) =>
+        sanitizePlaygroundInputs({ ...prev, ...importedConfig.inputs }),
+      );
     }
     if (importedConfig.parameterEnabled) {
       setParameterEnabled((prev) => ({