feat: 新增max_tokens和调整
parent
8703b8bdc7
commit
3df2e753c5
|
|
@ -412,6 +412,7 @@ CREATE TABLE biz_llm_models (
|
|||
model_code VARCHAR(100),
|
||||
temperature DECIMAL(3,2) DEFAULT 0.7,
|
||||
top_p DECIMAL(3,2) DEFAULT 0.9,
|
||||
max_tokens BIGINT NOT NULL DEFAULT 30000,
|
||||
is_default SMALLINT DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
status SMALLINT DEFAULT 1,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
|
|
@ -32,6 +33,9 @@ public class AiModelDTO {
|
|||
private BigDecimal temperature;
|
||||
@Schema(description = "TopP 参数")
|
||||
private BigDecimal topP;
|
||||
@JsonProperty("max_tokens")
|
||||
@Schema(description = "最大输出 token 数")
|
||||
private Long maxTokens;
|
||||
@Schema(description = "媒体配置")
|
||||
private Map<String, Object> mediaConfig;
|
||||
@Schema(description = "是否默认")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
|
|
@ -33,6 +34,9 @@ public class AiModelVO {
|
|||
private BigDecimal temperature;
|
||||
@Schema(description = "TopP 参数")
|
||||
private BigDecimal topP;
|
||||
@JsonProperty("max_tokens")
|
||||
@Schema(description = "最大输出 token 数")
|
||||
private Long maxTokens;
|
||||
@Schema(description = "多媒体配置")
|
||||
private Map<String, Object> mediaConfig;
|
||||
@Schema(description = "是否为默认模型")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.imeeting.entity.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
|
|
@ -43,6 +44,10 @@ public class LlmModel extends BaseEntity {
|
|||
@Schema(description = "Top P参数")
|
||||
private BigDecimal topP;
|
||||
|
||||
@TableField("max_tokens")
|
||||
@Schema(description = "最大输出 token 数")
|
||||
private Long maxTokens;
|
||||
|
||||
@Schema(description = "是否默认模型")
|
||||
private Integer isDefault;
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
private static final String MEDIA_TENCENT_OFFLINE_MODEL_CODE = "tencentOfflineModelCode";
|
||||
private static final String MEDIA_TENCENT_REALTIME_MODEL_CODE = "tencentRealtimeModelCode";
|
||||
private static final int DEFAULT_SORT_ORDER = 0;
|
||||
private static final long DEFAULT_LLM_MAX_TOKENS = 30000L;
|
||||
private static final String DEFAULT_LLM_API_PATH = "/v1/chat/completions";
|
||||
private static final String DEFAULT_ANTHROPIC_API_PATH = "/messages";
|
||||
private static final String CONNECTIVITY_TEST_SYSTEM_PROMPT = """
|
||||
|
|
@ -397,7 +398,7 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("model", dto.getModelCode().trim());
|
||||
body.put("stream", false);
|
||||
body.put("max_tokens", 32);
|
||||
body.put("max_tokens", resolveConnectivityMaxTokens(dto.getMaxTokens()));
|
||||
if (dto.getTemperature() != null) {
|
||||
body.put("temperature", dto.getTemperature());
|
||||
}
|
||||
|
|
@ -425,7 +426,7 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("model", dto.getModelCode().trim());
|
||||
body.put("system", CONNECTIVITY_TEST_SYSTEM_PROMPT);
|
||||
body.put("max_tokens", 32);
|
||||
body.put("max_tokens", resolveConnectivityMaxTokens(dto.getMaxTokens()));
|
||||
body.put("messages", List.of(
|
||||
Map.of("role", "user", "content", resolveTestMessage(dto.getTestMessage()))
|
||||
));
|
||||
|
|
@ -461,7 +462,7 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
"parts", List.of(Map.of("text", resolveTestMessage(dto.getTestMessage())))
|
||||
)
|
||||
));
|
||||
if (dto.getTemperature() != null || dto.getTopP() != null) {
|
||||
if (dto.getTemperature() != null || dto.getTopP() != null || dto.getMaxTokens() != null) {
|
||||
Map<String, Object> generationConfig = new LinkedHashMap<>();
|
||||
if (dto.getTemperature() != null) {
|
||||
generationConfig.put("temperature", dto.getTemperature());
|
||||
|
|
@ -469,6 +470,9 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
if (dto.getTopP() != null) {
|
||||
generationConfig.put("topP", dto.getTopP());
|
||||
}
|
||||
if (dto.getMaxTokens() != null) {
|
||||
generationConfig.put("maxOutputTokens", resolveConnectivityMaxTokens(dto.getMaxTokens()));
|
||||
}
|
||||
body.put("generationConfig", generationConfig);
|
||||
}
|
||||
|
||||
|
|
@ -874,6 +878,11 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
if (Integer.valueOf(1).equals(dto.getIsDefault()) && !Integer.valueOf(1).equals(dto.getStatus())) {
|
||||
throw new RuntimeException("默认模型必须为启用状态");
|
||||
}
|
||||
if (TYPE_LLM.equals(normalizeType(dto.getModelType()))
|
||||
&& dto.getMaxTokens() != null
|
||||
&& dto.getMaxTokens() <= 0) {
|
||||
throw new RuntimeException("max_tokens 必须为正整数");
|
||||
}
|
||||
validateTencentAsrConfig(dto);
|
||||
// if ("custom".equals(normalizeProvider(dto.getProvider()))) {
|
||||
// if (TYPE_ASR.equals(normalizeType(dto.getModelType()))) {
|
||||
|
|
@ -1178,12 +1187,21 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
entity.setModelCode(dto.getModelCode());
|
||||
entity.setTemperature(dto.getTemperature() == null ? BigDecimal.valueOf(0.2) : dto.getTemperature());
|
||||
entity.setTopP(dto.getTopP() == null ? BigDecimal.valueOf(0.9) : dto.getTopP());
|
||||
entity.setMaxTokens(resolveMaxTokens(dto.getMaxTokens()));
|
||||
entity.setIsDefault(dto.getIsDefault());
|
||||
entity.setStatus(dto.getStatus());
|
||||
entity.setSortOrder(normalizeSortOrder(dto.getSortOrder()));
|
||||
entity.setRemark(dto.getRemark());
|
||||
}
|
||||
|
||||
private Long resolveMaxTokens(Long maxTokens) {
|
||||
return maxTokens == null ? DEFAULT_LLM_MAX_TOKENS : maxTokens;
|
||||
}
|
||||
|
||||
private Long resolveConnectivityMaxTokens(Long maxTokens) {
|
||||
return maxTokens == null ? 32L : maxTokens;
|
||||
}
|
||||
|
||||
private AiModelVO toAsrVO(AsrModel entity) {
|
||||
return toAsrVO(entity, null, true);
|
||||
}
|
||||
|
|
@ -1281,6 +1299,7 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
vo.setModelCode(entity.getModelCode());
|
||||
vo.setTemperature(entity.getTemperature());
|
||||
vo.setTopP(entity.getTopP());
|
||||
vo.setMaxTokens(resolveMaxTokens(entity.getMaxTokens()));
|
||||
vo.setIsDefault(entity.getIsDefault());
|
||||
vo.setStatus(entity.getStatus());
|
||||
vo.setScope(Long.valueOf(0L).equals(entity.getTenantId()) ? "PLATFORM" : "TENANT");
|
||||
|
|
|
|||
|
|
@ -1325,7 +1325,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
Map<String, Object> req = new HashMap<>();
|
||||
req.put("model", llmModel.getModelCode());
|
||||
req.put("temperature", llmModel.getTemperature());
|
||||
req.put("max_tokens", 30000);
|
||||
req.put("max_tokens", llmModel.getMaxTokens() == null ? 30000L : llmModel.getMaxTokens());
|
||||
req.put("messages", List.of(
|
||||
Map.of("role", "system", "content", meetingSummaryPromptAssembler.buildSystemMessage(taskRecord.getTaskConfig())),
|
||||
Map.of("role", "user", "content", meetingSummaryPromptAssembler.buildUserMessage(taskRecord.getTaskConfig(), meeting, summarySource, userPrompt))
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ public class MeetingExternalSummaryWebhookTrigger {
|
|||
config.put("modelCode", model.getModelCode());
|
||||
config.put("temperature", model.getTemperature());
|
||||
config.put("topP", model.getTopP());
|
||||
config.put("max_tokens", model.getMaxTokens() == null ? 30000L : model.getMaxTokens());
|
||||
return config;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
|||
Map<String, Object> requestBody = new LinkedHashMap<>();
|
||||
requestBody.put("model", llmModel.getModelCode());
|
||||
requestBody.put("temperature", llmModel.getTemperature());
|
||||
requestBody.put("max_tokens", 30000);
|
||||
requestBody.put("max_tokens", llmModel.getMaxTokens() == null ? 30000L : llmModel.getMaxTokens());
|
||||
requestBody.put("messages", List.of(
|
||||
Map.of("role", "system", "content", renderChapterSystemPrompt()),
|
||||
Map.of("role", "user", "content", renderChapterUserPrompt(transcripts))
|
||||
|
|
@ -1015,7 +1015,7 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
|||
Map<String, Object> requestBody = new LinkedHashMap<>();
|
||||
requestBody.put("model", llmModel.getModelCode());
|
||||
requestBody.put("temperature", llmModel.getTemperature());
|
||||
requestBody.put("max_tokens", 30000);
|
||||
requestBody.put("max_tokens", llmModel.getMaxTokens() == null ? 30000L : llmModel.getMaxTokens());
|
||||
requestBody.put("messages", List.of(
|
||||
Map.of("role", "system", "content", renderChapterSystemPrompt()),
|
||||
Map.of("role", "user", "content", renderChapterUserPrompt(transcripts))
|
||||
|
|
|
|||
|
|
@ -481,6 +481,64 @@ class AiModelServiceImplTest {
|
|||
assertEquals(7, captor.getValue().getSortOrder());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveModelShouldPersistMaxTokensForLlm() throws Exception {
|
||||
AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
|
||||
LlmModelMapper llmModelMapper = mock(LlmModelMapper.class);
|
||||
when(llmModelMapper.insert(any(LlmModel.class))).thenReturn(1);
|
||||
|
||||
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||
objectMapper,
|
||||
asrModelMapper,
|
||||
llmModelMapper
|
||||
);
|
||||
|
||||
AiModelDTO dto = new AiModelDTO();
|
||||
dto.setModelType("LLM");
|
||||
dto.setModelName("max-token-llm");
|
||||
dto.setProvider("openai");
|
||||
dto.setBaseUrl("http://127.0.0.1:9000");
|
||||
dto.setApiPath("/v1/chat/completions");
|
||||
dto.setModelCode("gpt-test");
|
||||
dto.setMaxTokens(8192L);
|
||||
dto.setIsDefault(0);
|
||||
dto.setStatus(1);
|
||||
|
||||
AiModelVO result = service.saveModel(dto);
|
||||
|
||||
ArgumentCaptor<LlmModel> captor = ArgumentCaptor.forClass(LlmModel.class);
|
||||
verify(llmModelMapper, times(1)).insert(captor.capture());
|
||||
assertEquals(8192L, captor.getValue().getMaxTokens());
|
||||
assertEquals(8192L, result.getMaxTokens());
|
||||
|
||||
JsonNode json = objectMapper.readTree(objectMapper.writeValueAsString(result));
|
||||
assertEquals(8192L, json.path("max_tokens").asLong());
|
||||
assertEquals(false, json.has("maxTokens"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveModelShouldRejectNonPositiveMaxTokensForLlm() {
|
||||
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||
objectMapper,
|
||||
mock(AsrModelMapper.class),
|
||||
mock(LlmModelMapper.class)
|
||||
);
|
||||
|
||||
AiModelDTO dto = new AiModelDTO();
|
||||
dto.setModelType("LLM");
|
||||
dto.setModelName("invalid-max-token-llm");
|
||||
dto.setProvider("openai");
|
||||
dto.setBaseUrl("http://127.0.0.1:9000");
|
||||
dto.setApiPath("/v1/chat/completions");
|
||||
dto.setModelCode("gpt-test");
|
||||
dto.setMaxTokens(0L);
|
||||
dto.setIsDefault(0);
|
||||
dto.setStatus(1);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> service.saveModel(dto));
|
||||
assertEquals("max_tokens 必须为正整数", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveModelShouldRejectDisabledDefaultModel() {
|
||||
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface AiModelVO {
|
|||
wsUrl?: string;
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
max_tokens?: number;
|
||||
mediaConfig?: Record<string, any>;
|
||||
isDefault: number;
|
||||
status: number;
|
||||
|
|
@ -47,6 +48,7 @@ export interface AiModelDTO {
|
|||
wsUrl?: string;
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
max_tokens?: number;
|
||||
mediaConfig?: Record<string, any>;
|
||||
isDefault: number;
|
||||
status: number;
|
||||
|
|
@ -157,6 +159,7 @@ export const testLlmModelConnectivity = (data: {
|
|||
modelCode: string;
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
max_tokens?: number;
|
||||
testMessage: string;
|
||||
}) => {
|
||||
return http.post<{ code: string; data: boolean; msg: string }>(
|
||||
|
|
|
|||
|
|
@ -323,6 +323,7 @@ const AiModels: React.FC = () => {
|
|||
: undefined,
|
||||
temperature: values.temperature,
|
||||
topP: values.topP,
|
||||
max_tokens: values.max_tokens,
|
||||
isDefault: values.isDefaultChecked ? 1 : 0,
|
||||
status: values.statusChecked ? 1 : 0,
|
||||
sortOrder: values.sortOrder ?? 0,
|
||||
|
|
@ -347,8 +348,8 @@ const AiModels: React.FC = () => {
|
|||
|
||||
const handleTestConnectivity = async () => {
|
||||
if (activeType === "LLM") {
|
||||
const values = await form.validateFields(["provider", "baseUrl", "apiPath", "modelCode"]);
|
||||
const extraValues = form.getFieldsValue(["apiKey", "temperature", "topP"]);
|
||||
const values = await form.validateFields(["provider", "baseUrl", "apiPath", "modelCode", "max_tokens"]);
|
||||
const extraValues = form.getFieldsValue(["apiKey", "temperature", "topP", "max_tokens"]);
|
||||
setConnectivityLoading(true);
|
||||
try {
|
||||
await testLlmModelConnectivity({
|
||||
|
|
@ -359,6 +360,7 @@ const AiModels: React.FC = () => {
|
|||
modelCode: values.modelCode,
|
||||
temperature: extraValues.temperature,
|
||||
topP: extraValues.topP,
|
||||
max_tokens: extraValues.max_tokens,
|
||||
testMessage: DEFAULT_LLM_TEST_MESSAGE,
|
||||
});
|
||||
message.success("LLM 连通性测试成功");
|
||||
|
|
@ -773,16 +775,38 @@ const AiModels: React.FC = () => {
|
|||
<Input />
|
||||
</Form.Item>
|
||||
<Row gutter={16} className="app-responsive-form-row">
|
||||
<Col xs={24} md={12}>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="temperature" label="Temperature">
|
||||
<InputNumber min={0} max={2} step={0.1} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="topP" label="Top P">
|
||||
<InputNumber min={0} max={1} step={0.1} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item
|
||||
name="max_tokens"
|
||||
label="max_tokens"
|
||||
rules={[
|
||||
{ required: true, message: "请输入 max_tokens" },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (Number.isInteger(value) && value > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error("max_tokens 必须为正整数"));
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={0} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue