Browse Source

Merge pull request #2627 from seefs001/feature/channel-test-param-override

feat: channel testing supports parameter overriding
Seefs 1 month ago
parent
commit
6169f46cc6

+ 22 - 0
controller/channel-test.go

@@ -193,6 +193,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
 		}
 		}
 	}
 	}
 
 
+	info.IsChannelTest = true
 	info.InitChannelMeta(c)
 	info.InitChannelMeta(c)
 
 
 	err = helper.ModelMappedHelper(c, info, request)
 	err = helper.ModelMappedHelper(c, info, request)
@@ -309,6 +310,27 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
 			newAPIError: types.NewError(err, types.ErrorCodeJsonMarshalFailed),
 			newAPIError: types.NewError(err, types.ErrorCodeJsonMarshalFailed),
 		}
 		}
 	}
 	}
+
+	//jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
+	//if err != nil {
+	//	return testResult{
+	//		context:     c,
+	//		localErr:    err,
+	//		newAPIError: types.NewError(err, types.ErrorCodeConvertRequestFailed),
+	//	}
+	//}
+
+	if len(info.ParamOverride) > 0 {
+		jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride, relaycommon.BuildParamOverrideContext(info))
+		if err != nil {
+			return testResult{
+				context:     c,
+				localErr:    err,
+				newAPIError: types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid),
+			}
+		}
+	}
+
 	requestBody := bytes.NewBuffer(jsonData)
 	requestBody := bytes.NewBuffer(jsonData)
 	c.Request.Body = io.NopCloser(requestBody)
 	c.Request.Body = io.NopCloser(requestBody)
 	resp, err := adaptor.DoRequest(c, info, requestBody)
 	resp, err := adaptor.DoRequest(c, info, requestBody)

+ 14 - 8
relay/common/override.go

@@ -570,18 +570,19 @@ func mergeObjects(jsonStr, path string, value interface{}, keepOrigin bool) (str
 
 
 // BuildParamOverrideContext 提供 ApplyParamOverride 可用的上下文信息。
 // BuildParamOverrideContext 提供 ApplyParamOverride 可用的上下文信息。
 // 目前内置以下字段:
 // 目前内置以下字段:
-//   - model:优先使用上游模型名(UpstreamModelName),若不存在则回落到原始模型名(OriginModelName)。
-//   - upstream_model:始终为通道映射后的上游模型名。
+//   - upstream_model/model:始终为通道映射后的上游模型名。
 //   - original_model:请求最初指定的模型名。
 //   - original_model:请求最初指定的模型名。
+//   - request_path:请求路径
+//   - is_channel_test:是否为渠道测试请求(同 is_test)。
 func BuildParamOverrideContext(info *RelayInfo) map[string]interface{} {
 func BuildParamOverrideContext(info *RelayInfo) map[string]interface{} {
-	if info == nil || info.ChannelMeta == nil {
+	if info == nil {
 		return nil
 		return nil
 	}
 	}
 
 
 	ctx := make(map[string]interface{})
 	ctx := make(map[string]interface{})
-	if info.UpstreamModelName != "" {
-		ctx["model"] = info.UpstreamModelName
-		ctx["upstream_model"] = info.UpstreamModelName
+	if info.ChannelMeta != nil && info.ChannelMeta.UpstreamModelName != "" {
+		ctx["model"] = info.ChannelMeta.UpstreamModelName
+		ctx["upstream_model"] = info.ChannelMeta.UpstreamModelName
 	}
 	}
 	if info.OriginModelName != "" {
 	if info.OriginModelName != "" {
 		ctx["original_model"] = info.OriginModelName
 		ctx["original_model"] = info.OriginModelName
@@ -590,8 +591,13 @@ func BuildParamOverrideContext(info *RelayInfo) map[string]interface{} {
 		}
 		}
 	}
 	}
 
 
-	if len(ctx) == 0 {
-		return nil
+	if info.RequestURLPath != "" {
+		requestPath := info.RequestURLPath
+		if requestPath != "" {
+			ctx["request_path"] = requestPath
+		}
 	}
 	}
+
+	ctx["is_channel_test"] = info.IsChannelTest
 	return ctx
 	return ctx
 }
 }

+ 1 - 0
relay/common/relay_info.go

@@ -115,6 +115,7 @@ type RelayInfo struct {
 	SendResponseCount      int
 	SendResponseCount      int
 	FinalPreConsumedQuota  int  // 最终预消耗的配额
 	FinalPreConsumedQuota  int  // 最终预消耗的配额
 	IsClaudeBetaQuery      bool // /v1/messages?beta=true
 	IsClaudeBetaQuery      bool // /v1/messages?beta=true
+	IsChannelTest          bool // channel test request
 
 
 	PriceData types.PriceData
 	PriceData types.PriceData
 
 

+ 29 - 9
web/src/components/table/channels/modals/EditChannelModal.jsx

@@ -199,17 +199,11 @@ const EditChannelModal = (props) => {
     if (!trimmed) return [];
     if (!trimmed) return [];
     try {
     try {
       const parsed = JSON.parse(trimmed);
       const parsed = JSON.parse(trimmed);
-      if (
-        !parsed ||
-        typeof parsed !== 'object' ||
-        Array.isArray(parsed)
-      ) {
+      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
         return [];
         return [];
       }
       }
       const values = Object.values(parsed)
       const values = Object.values(parsed)
-        .map((value) =>
-          typeof value === 'string' ? value.trim() : undefined,
-        )
+        .map((value) => (typeof value === 'string' ? value.trim() : undefined))
         .filter((value) => value);
         .filter((value) => value);
       return Array.from(new Set(values));
       return Array.from(new Set(values));
     } catch (error) {
     } catch (error) {
@@ -509,6 +503,18 @@ const EditChannelModal = (props) => {
     //setAutoBan
     //setAutoBan
   };
   };
 
 
+  const formatJsonField = (fieldName) => {
+    const rawValue = (inputs?.[fieldName] ?? '').trim();
+    if (!rawValue) return;
+
+    try {
+      const parsed = JSON.parse(rawValue);
+      handleInputChange(fieldName, JSON.stringify(parsed, null, 2));
+    } catch (error) {
+      showError(`${t('JSON格式错误')}: ${error.message}`);
+    }
+  };
+
   const loadChannel = async () => {
   const loadChannel = async () => {
     setLoading(true);
     setLoading(true);
     let res = await API.get(`/api/channel/${channelId}`);
     let res = await API.get(`/api/channel/${channelId}`);
@@ -2812,6 +2818,12 @@ const EditChannelModal = (props) => {
                           >
                           >
                             {t('新格式模板')}
                             {t('新格式模板')}
                           </Text>
                           </Text>
+                          <Text
+                            className='!text-semi-color-primary cursor-pointer'
+                            onClick={() => formatJsonField('param_override')}
+                          >
+                            {t('格式化')}
+                          </Text>
                         </div>
                         </div>
                       }
                       }
                       showClear
                       showClear
@@ -2852,6 +2864,12 @@ const EditChannelModal = (props) => {
                             >
                             >
                               {t('填入模板')}
                               {t('填入模板')}
                             </Text>
                             </Text>
+                            <Text
+                              className='!text-semi-color-primary cursor-pointer'
+                              onClick={() => formatJsonField('header_override')}
+                            >
+                              {t('格式化')}
+                            </Text>
                           </div>
                           </div>
                           <div>
                           <div>
                             <Text type='tertiary' size='small'>
                             <Text type='tertiary' size='small'>
@@ -3181,7 +3199,9 @@ const EditChannelModal = (props) => {
             ? inputs.models.map(String)
             ? inputs.models.map(String)
             : [];
             : [];
           const incoming = modelIds.map(String);
           const incoming = modelIds.map(String);
-          const nextModels = Array.from(new Set([...existingModels, ...incoming]));
+          const nextModels = Array.from(
+            new Set([...existingModels, ...incoming]),
+          );
 
 
           handleInputChange('models', nextModels);
           handleInputChange('models', nextModels);
           if (formApiRef.current) {
           if (formApiRef.current) {