Compare commits

...

24 Commits

Author SHA1 Message Date
CaIon
f613a79f3e feat: Enhance image request validation in relay-image.go: set default model and size, improve error handling for size format, and ensure prompt and N parameters are validated correctly. 2025-05-28 20:18:37 +08:00
IcedTangerine
87540b4f7c Merge pull request #1110 from wangr0031/fix_parallel_tool_calls
feat: chat/completion路由透传parallel_tool_calls参数
2025-05-28 14:25:43 +08:00
IcedTangerine
e3d7b31a49 Update openai_request.go 2025-05-28 14:25:24 +08:00
IcedTangerine
bf016543c3 Merge pull request #1113 from tbphp/tbphp_vertex_gemini_global_region
fix: Vertex channel global region format
2025-05-28 14:16:47 +08:00
IcedTangerine
eb94aa13e6 Merge pull request #1111 from feitianbubu/fxm-ali-fetch-models-url
fix: ali FetchUpstreamModels url
2025-05-28 14:11:17 +08:00
tbphp
6e72dcd0ba fix: Vertex channel global region format 2025-05-27 21:50:53 +08:00
skynono
96ab4177ca fix: ali FetchUpstreamModels url 2025-05-27 11:22:40 +08:00
wang.rong
76824a0337 chat/completion透传parallel_tool_calls参数 2025-05-27 09:32:20 +08:00
IcedTangerine
3cd29a4963 Merge pull request #1109 from feitianbubu/fix-qwen-thinking
fix: ali parameter.enable_thinking must be set to false for non-strea…
2025-05-26 19:32:34 +08:00
creamlike1024
41120b4d75 Merge branch 'main' of github.com:QuantumNous/new-api 2025-05-26 18:56:14 +08:00
creamlike1024
30d5a11f46 fix: search-preview model web search billing 2025-05-26 18:53:41 +08:00
skynono
368fd75c86 fix: ali parameter.enable_thinking must be set to false for non-streaming calls 2025-05-26 17:41:02 +08:00
IcedTangerine
ee07762611 Merge pull request #1075 from feitianbubu/fix-default-model-not-exist
fix: if default model is not exist, set the first one as default
2025-05-26 17:21:14 +08:00
IcedTangerine
a215538b4d Merge pull request #1081 from feitianbubu/fixTypoOidcEnabledField
fix: typo in oidc_enabled field (previously oidc)
2025-05-26 17:20:35 +08:00
IcedTangerine
873e3f3dc8 Merge pull request #1099 from feitianbubu/fixTagModeStatusSave
fix: keep BatchDelete and TagMode enabled status
2025-05-26 17:17:34 +08:00
Calcium-Ion
b564cac048 Merge pull request #1100 from daggeryu/patch-4
fix aws claude-sonnet-4-20250514
2025-05-24 15:27:30 +08:00
CaIon
fbdad581b5 fix: improve input validation and error handling in ModelSetting and SettingGeminiModel components 2025-05-24 15:26:55 +08:00
daggeryu
0595636ceb fix aws claude-sonnet-4-20250514 2025-05-24 01:21:14 +08:00
CaIon
d95c2436d7 feat: add support for new regions in Claude Sonnet 4 and Claude Opus 4 models in AWS constants 2025-05-23 21:11:00 +08:00
skynono
2cc2d4f652 fix: keep BatchDelete and TagMode enabled status 2025-05-23 20:17:48 +08:00
CaIon
1644b7b15d feat: add new model entries for Claude Sonnet 4 and Claude Opus 4 across multiple components, including constants and cache settings 2025-05-23 15:20:16 +08:00
CaIon
66a8612d12 feat: add new model ratios for Claude Sonnet 4 and Claude Opus 4; update ratio retrieval logic for improved handling of model names 2025-05-23 02:02:21 +08:00
skynono
e1190f98e9 fix: typo in oidc_enabled field (previously oidc) 2025-05-21 09:33:57 +08:00
skynono
9c12e02cb5 fix: if default model is not exist, set the first one as default 2025-05-19 14:56:39 +08:00
15 changed files with 202 additions and 114 deletions

View File

@@ -119,8 +119,11 @@ func FetchUpstreamModels(c *gin.Context) {
baseURL = channel.GetBaseURL()
}
url := fmt.Sprintf("%s/v1/models", baseURL)
if channel.Type == common.ChannelTypeGemini {
switch channel.Type {
case common.ChannelTypeGemini:
url = fmt.Sprintf("%s/v1beta/openai/models", baseURL)
case common.ChannelTypeAli:
url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
}
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
if err != nil {

View File

@@ -43,6 +43,7 @@ type GeneralOpenAIRequest struct {
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
EncodingFormat any `json:"encoding_format,omitempty"`
Seed float64 `json:"seed,omitempty"`
ParallelTooCalls bool `json:"parallel_tool_calls,omitempty"`
Tools []ToolCallRequest `json:"tools,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"`
User string `json:"user,omitempty"`
@@ -53,6 +54,7 @@ type GeneralOpenAIRequest struct {
Audio any `json:"audio,omitempty"`
EnableThinking any `json:"enable_thinking,omitempty"` // ali
ExtraBody any `json:"extra_body,omitempty"`
WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
}
type ToolCallRequest struct {
@@ -371,6 +373,11 @@ func (m *Message) ParseContent() []MediaContent {
return contentList
}
type WebSearchOptions struct {
SearchContextSize string `json:"search_context_size,omitempty"`
UserLocation json.RawMessage `json:"user_location,omitempty"`
}
type OpenAIResponsesRequest struct {
Model string `json:"model"`
Input json.RawMessage `json:"input,omitempty"`

View File

@@ -57,6 +57,12 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
if request == nil {
return nil, errors.New("request is nil")
}
// fix: ali parameter.enable_thinking must be set to false for non-streaming calls
if !info.IsStream {
request.EnableThinking = false
}
switch info.RelayMode {
default:
aliReq := requestOpenAI2Ali(*request)

View File

@@ -11,6 +11,8 @@ var awsModelIDMap = map[string]string{
"claude-3-5-sonnet-20241022": "anthropic.claude-3-5-sonnet-20241022-v2:0",
"claude-3-5-haiku-20241022": "anthropic.claude-3-5-haiku-20241022-v1:0",
"claude-3-7-sonnet-20250219": "anthropic.claude-3-7-sonnet-20250219-v1:0",
"claude-sonnet-4-20250514": "anthropic.claude-sonnet-4-20250514-v1:0",
"claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0",
}
var awsModelCanCrossRegionMap = map[string]map[string]bool{
@@ -41,6 +43,16 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{
},
"anthropic.claude-3-7-sonnet-20250219-v1:0": {
"us": true,
"ap": true,
"eu": true,
},
"anthropic.claude-sonnet-4-20250514-v1:0": {
"us": true,
"ap": true,
"eu": true,
},
"anthropic.claude-opus-4-20250514-v1:0": {
"us": true,
},
}

View File

@@ -13,6 +13,10 @@ var ModelList = []string{
"claude-3-5-sonnet-20241022",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-20250219-thinking",
"claude-sonnet-4-20250514",
"claude-sonnet-4-20250514-thinking",
"claude-opus-4-20250514",
"claude-opus-4-20250514-thinking",
}
var ChannelName = "claude"

View File

@@ -31,6 +31,8 @@ var claudeModelMap = map[string]string{
"claude-3-5-sonnet-20240620": "claude-3-5-sonnet@20240620",
"claude-3-5-sonnet-20241022": "claude-3-5-sonnet-v2@20241022",
"claude-3-7-sonnet-20250219": "claude-3-7-sonnet@20250219",
"claude-sonnet-4-20250514": "claude-sonnet-4@20250514",
"claude-opus-4-20250514": "claude-opus-4@20250514",
}
const anthropicVersion = "vertex-2023-10-16"
@@ -93,14 +95,23 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
} else {
suffix = "generateContent"
}
return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s",
region,
adc.ProjectID,
region,
info.UpstreamModelName,
suffix,
), nil
if region == "global" {
return fmt.Sprintf(
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/google/models/%s:%s",
adc.ProjectID,
info.UpstreamModelName,
suffix,
), nil
} else {
return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s",
region,
adc.ProjectID,
region,
info.UpstreamModelName,
suffix,
), nil
}
} else if a.RequestMode == RequestModeClaude {
if info.IsStream {
suffix = "streamRawPredict?alt=sse"

View File

@@ -46,11 +46,23 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
if err != nil {
return nil, err
}
if imageRequest.Model == "" {
imageRequest.Model = "dall-e-3"
}
if strings.Contains(imageRequest.Size, "×") {
return nil, errors.New("size an unexpected error occurred in the parameter, please use 'x' instead of the multiplication sign '×'")
}
// Not "256x256", "512x512", or "1024x1024"
if imageRequest.Model == "dall-e-2" || imageRequest.Model == "dall-e" {
if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" {
return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024 for dall-e-2 or dall-e")
}
if imageRequest.Size == "" {
imageRequest.Size = "1024x1024"
}
} else if imageRequest.Model == "dall-e-3" {
if imageRequest.Size != "" && imageRequest.Size != "1024x1024" && imageRequest.Size != "1024x1792" && imageRequest.Size != "1792x1024" {
return nil, errors.New("size must be one of 1024x1024, 1024x1792 or 1792x1024 for dall-e-3")
@@ -58,74 +70,24 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
if imageRequest.Quality == "" {
imageRequest.Quality = "standard"
}
// N should between 1 and 10
//if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) {
// return service.OpenAIErrorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
//}
if imageRequest.Size == "" {
imageRequest.Size = "1024x1024"
}
} else if imageRequest.Model == "gpt-image-1" {
if imageRequest.Quality == "" {
imageRequest.Quality = "auto"
}
}
if imageRequest.Prompt == "" {
return nil, errors.New("prompt is required")
}
if imageRequest.N == 0 {
imageRequest.N = 1
}
}
if imageRequest.Prompt == "" {
return nil, errors.New("prompt is required")
}
if imageRequest.Model == "" {
imageRequest.Model = "dall-e-2"
}
if strings.Contains(imageRequest.Size, "×") {
return nil, errors.New("size an unexpected error occurred in the parameter, please use 'x' instead of the multiplication sign '×'")
}
if imageRequest.N == 0 {
imageRequest.N = 1
}
if imageRequest.Size == "" {
imageRequest.Size = "1024x1024"
}
err := common.UnmarshalBodyReusable(c, imageRequest)
if err != nil {
return nil, err
}
if imageRequest.Prompt == "" {
return nil, errors.New("prompt is required")
}
if strings.Contains(imageRequest.Size, "×") {
return nil, errors.New("size an unexpected error occurred in the parameter, please use 'x' instead of the multiplication sign '×'")
}
if imageRequest.N == 0 {
imageRequest.N = 1
}
if imageRequest.Size == "" {
imageRequest.Size = "1024x1024"
}
if imageRequest.Model == "" {
imageRequest.Model = "dall-e-2"
}
// x.ai grok-2-image not support size, quality or style
if imageRequest.Size == "empty" {
imageRequest.Size = ""
}
// Not "256x256", "512x512", or "1024x1024"
if imageRequest.Model == "dall-e-2" || imageRequest.Model == "dall-e" {
if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" {
return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024")
}
} else if imageRequest.Model == "dall-e-3" {
if imageRequest.Size != "" && imageRequest.Size != "1024x1024" && imageRequest.Size != "1024x1792" && imageRequest.Size != "1792x1024" {
return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024")
}
if imageRequest.Quality == "" {
imageRequest.Quality = "standard"
}
//if imageRequest.N != 1 {
// return nil, errors.New("n must be 1")
//}
}
// N should between 1 and 10
//if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) {
// return service.OpenAIErrorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
//}
if setting.ShouldCheckPromptSensitive() {
words, err := service.CheckSensitiveInput(imageRequest.Prompt)
if err != nil {
@@ -229,6 +191,10 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
requestBody = bytes.NewBuffer(jsonData)
}
if common.DebugEnabled {
println(fmt.Sprintf("image request body: %s", requestBody))
}
statusCodeMappingStr := c.GetString("status_code_mapping")
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)

View File

@@ -47,6 +47,20 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
if textRequest.Model == "" {
return nil, errors.New("model is required")
}
if textRequest.WebSearchOptions != nil {
if textRequest.WebSearchOptions.SearchContextSize != "" {
validSizes := map[string]bool{
"high": true,
"medium": true,
"low": true,
}
if !validSizes[textRequest.WebSearchOptions.SearchContextSize] {
return nil, errors.New("invalid search_context_size, must be one of: high, medium, low")
}
} else {
textRequest.WebSearchOptions.SearchContextSize = "medium"
}
}
switch relayInfo.RelayMode {
case relayconstant.RelayModeCompletions:
if textRequest.Prompt == "" {
@@ -76,6 +90,10 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
// get & validate textRequest 获取并验证文本请求
textRequest, err := getAndValidateTextRequest(c, relayInfo)
if textRequest.WebSearchOptions != nil {
c.Set("chat_completion_web_search_context_size", textRequest.WebSearchOptions.SearchContextSize)
}
if err != nil {
common.LogError(c, fmt.Sprintf("getAndValidateTextRequest failed: %s", err.Error()))
return service.OpenAIErrorWrapperLocal(err, "invalid_text_request", http.StatusBadRequest)
@@ -370,9 +388,20 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
dWebSearchQuota = decimal.NewFromFloat(webSearchPrice).
Mul(decimal.NewFromInt(int64(webSearchTool.CallCount))).
Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
extraContent += fmt.Sprintf("Web Search 调用 %d 次,上下文大小 %s调用花费 $%s",
extraContent += fmt.Sprintf("Web Search 调用 %d 次,上下文大小 %s调用花费 %s",
webSearchTool.CallCount, webSearchTool.SearchContextSize, dWebSearchQuota.String())
}
} else if strings.HasSuffix(modelName, "search-preview") {
// search-preview 模型不支持 response api
searchContextSize := ctx.GetString("chat_completion_web_search_context_size")
if searchContextSize == "" {
searchContextSize = "medium"
}
webSearchPrice = operation_setting.GetWebSearchPricePerThousand(modelName, searchContextSize)
dWebSearchQuota = decimal.NewFromFloat(webSearchPrice).
Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
extraContent += fmt.Sprintf("Web Search 调用 1 次,上下文大小 %s调用花费 %s",
searchContextSize, dWebSearchQuota.String())
}
// file search tool 计费
var dFileSearchQuota decimal.Decimal
@@ -463,10 +492,16 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
other["image_ratio"] = imageRatio
other["image_output"] = imageTokens
}
if !dWebSearchQuota.IsZero() && relayInfo.ResponsesUsageInfo != nil {
if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists {
if !dWebSearchQuota.IsZero() {
if relayInfo.ResponsesUsageInfo != nil {
if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists {
other["web_search"] = true
other["web_search_call_count"] = webSearchTool.CallCount
other["web_search_price"] = webSearchPrice
}
} else if strings.HasSuffix(modelName, "search-preview") {
other["web_search"] = true
other["web_search_call_count"] = webSearchTool.CallCount
other["web_search_call_count"] = 1
other["web_search_price"] = webSearchPrice
}
}

View File

@@ -36,6 +36,10 @@ var defaultCacheRatio = map[string]float64{
"claude-3-5-sonnet-20241022": 0.1,
"claude-3-7-sonnet-20250219": 0.1,
"claude-3-7-sonnet-20250219-thinking": 0.1,
"claude-sonnet-4-20250514": 0.1,
"claude-sonnet-4-20250514-thinking": 0.1,
"claude-opus-4-20250514": 0.1,
"claude-opus-4-20250514-thinking": 0.1,
}
var defaultCreateCacheRatio = map[string]float64{
@@ -47,6 +51,10 @@ var defaultCreateCacheRatio = map[string]float64{
"claude-3-5-sonnet-20241022": 1.25,
"claude-3-7-sonnet-20250219": 1.25,
"claude-3-7-sonnet-20250219-thinking": 1.25,
"claude-sonnet-4-20250514": 1.25,
"claude-sonnet-4-20250514-thinking": 1.25,
"claude-opus-4-20250514": 1.25,
"claude-opus-4-20250514-thinking": 1.25,
}
//var defaultCreateCacheRatio = map[string]float64{}

View File

@@ -114,7 +114,9 @@ var defaultModelRatio = map[string]float64{
"claude-3-5-sonnet-20241022": 1.5,
"claude-3-7-sonnet-20250219": 1.5,
"claude-3-7-sonnet-20250219-thinking": 1.5,
"claude-sonnet-4-20250514": 1.5,
"claude-3-opus-20240229": 7.5, // $15 / 1M tokens
"claude-opus-4-20250514": 7.5,
"ERNIE-4.0-8K": 0.120 * RMB,
"ERNIE-3.5-8K": 0.012 * RMB,
"ERNIE-3.5-8K-0205": 0.024 * RMB,
@@ -440,13 +442,15 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
if name == "chatgpt-4o-latest" {
return 3, true
}
if strings.Contains(name, "claude-instant-1") {
return 3, true
} else if strings.Contains(name, "claude-2") {
return 3, true
} else if strings.Contains(name, "claude-3") {
if strings.Contains(name, "claude-3") {
return 5, true
} else if strings.Contains(name, "claude-sonnet-4") || strings.Contains(name, "claude-opus-4") {
return 5, true
} else if strings.Contains(name, "claude-instant-1") || strings.Contains(name, "claude-2") {
return 3, true
}
if strings.HasPrefix(name, "gpt-3.5") {
if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") {
// https://openai.com/blog/new-embedding-models-and-api-updates

View File

@@ -888,9 +888,13 @@ const ChannelsTable = () => {
const localIdSort = localStorage.getItem('id-sort') === 'true';
const localPageSize =
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
const localEnableTagMode = localStorage.getItem('enable-tag-mode') === 'true';
const localEnableBatchDelete = localStorage.getItem('enable-batch-delete') === 'true';
setIdSort(localIdSort);
setPageSize(localPageSize);
loadChannels(0, localPageSize, localIdSort, enableTagMode)
setEnableTagMode(localEnableTagMode);
setEnableBatchDelete(localEnableBatchDelete);
loadChannels(0, localPageSize, localIdSort, localEnableTagMode)
.then()
.catch((reason) => {
showError(reason);
@@ -1486,10 +1490,12 @@ const ChannelsTable = () => {
{t('开启批量操作')}
</Typography.Text>
<Switch
checked={enableBatchDelete}
label={t('开启批量操作')}
uncheckedText={t('关')}
aria-label={t('是否开启批量操作')}
onChange={(v) => {
localStorage.setItem('enable-batch-delete', v + '');
setEnableBatchDelete(v);
}}
/>
@@ -1553,6 +1559,7 @@ const ChannelsTable = () => {
uncheckedText={t('关')}
aria-label={t('是否启用标签聚合')}
onChange={(v) => {
localStorage.setItem('enable-tag-mode', v + '');
setEnableTagMode(v);
loadChannels(0, pageSize, idSort, v);
}}

View File

@@ -39,7 +39,9 @@ const ModelSetting = () => {
item.key === 'claude.default_max_tokens' ||
item.key === 'gemini.supported_imagine_models'
) {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
if (item.value !== '') {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
}
}
if (item.key.endsWith('Enabled') || item.key.endsWith('enabled')) {
newInputs[item.key] = item.value === 'true' ? true : false;
@@ -60,6 +62,7 @@ const ModelSetting = () => {
// showSuccess('刷新成功');
} catch (error) {
showError('刷新失败');
console.error(error);
} finally {
setLoading(false);
}

View File

@@ -158,7 +158,7 @@ const Home = () => {
</p>
<p>
{t('OIDC 身份验证')}
{statusState?.status?.oidc === true
{statusState?.status?.oidc_enabled === true
? t('已启用')
: t('未启用')}
</p>

View File

@@ -64,8 +64,9 @@ const Playground = () => {
},
];
const defaultModel = 'gpt-4o-mini';
const [inputs, setInputs] = useState({
model: 'gpt-4o-mini',
model: defaultModel,
group: '',
max_tokens: 0,
temperature: 0,
@@ -108,6 +109,11 @@ const Playground = () => {
value: model,
}));
setModels(localModelOptions);
// if default model is not in the list, set the first one as default
const hasDefault = localModelOptions.some(option => option.value === defaultModel);
if (!hasDefault && localModelOptions.length > 0) {
setInputs((inputs) => ({ ...inputs, model: localModelOptions[0].value }));
}
} else {
showError(t(message));
}

View File

@@ -27,40 +27,48 @@ export default function SettingGeminiModel(props) {
const [inputs, setInputs] = useState({
'gemini.safety_settings': '',
'gemini.version_settings': '',
'gemini.supported_imagine_models': [],
'gemini.supported_imagine_models': '',
'gemini.thinking_adapter_enabled': false,
'gemini.thinking_adapter_budget_tokens_percentage': 0.6,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = String(inputs[item.key]);
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
}
showSuccess(t('保存成功'));
props.refresh();
async function onSubmit() {
await refForm.current
.validate()
.then(() => {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = String(inputs[item.key]);
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
})
.catch(() => {
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
.catch((error) => {
console.error('Validation failed:', error);
showError(t('请检查输入'));
});
}
@@ -146,6 +154,14 @@ export default function SettingGeminiModel(props) {
label={t('支持的图像模型')}
placeholder={t('例如:') + '\n' + JSON.stringify(['gemini-2.0-flash-exp-image-generation'], null, 2)}
onChange={(value) => setInputs({ ...inputs, 'gemini.supported_imagine_models': value })}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串'),
},
]}
/>
</Col>
</Row>