diff --git a/relay/common/override.go b/relay/common/override.go index 8bfdcd74..af0b4361 100644 --- a/relay/common/override.go +++ b/relay/common/override.go @@ -21,10 +21,23 @@ var negativeIndexRegexp = regexp.MustCompile(`\.(-\d+)`) const ( paramOverrideContextRequestHeaders = "request_headers" paramOverrideContextHeaderOverride = "header_override" + paramOverrideContextAuditRecorder = "__param_override_audit_recorder" ) var errSourceHeaderNotFound = errors.New("source header does not exist") +var paramOverrideKeyAuditPaths = map[string]struct{}{ + "model": {}, + "original_model": {}, + "upstream_model": {}, + "service_tier": {}, + "inference_geo": {}, +} + +type paramOverrideAuditRecorder struct { + lines []string +} + type ConditionOperation struct { Path string `json:"path"` // JSON路径 Mode string `json:"mode"` // full, prefix, suffix, contains, gt, gte, lt, lte @@ -118,6 +131,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c if len(paramOverride) == 0 { return jsonData, nil } + auditRecorder := getParamOverrideAuditRecorder(conditionContext) // 尝试断言为操作格式 if operations, ok := tryParseOperations(paramOverride); ok { @@ -125,7 +139,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c workingJSON := jsonData var err error if len(legacyOverride) > 0 { - workingJSON, err = applyOperationsLegacy(workingJSON, legacyOverride) + workingJSON, err = applyOperationsLegacy(workingJSON, legacyOverride, auditRecorder) if err != nil { return nil, err } @@ -137,7 +151,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c } // 直接使用旧方法 - return applyOperationsLegacy(jsonData, paramOverride) + return applyOperationsLegacy(jsonData, paramOverride, auditRecorder) } func buildLegacyParamOverride(paramOverride map[string]interface{}) map[string]interface{} { @@ -161,14 +175,200 @@ func ApplyParamOverrideWithRelayInfo(jsonData []byte, info *RelayInfo) ([]byte, } overrideCtx := BuildParamOverrideContext(info) + var recorder *paramOverrideAuditRecorder + if shouldEnableParamOverrideAudit(paramOverride) { + recorder = ¶mOverrideAuditRecorder{} + overrideCtx[paramOverrideContextAuditRecorder] = recorder + } result, err := ApplyParamOverride(jsonData, paramOverride, overrideCtx) if err != nil { return nil, err } syncRuntimeHeaderOverrideFromContext(info, overrideCtx) + if info != nil { + if recorder != nil { + info.ParamOverrideAudit = recorder.lines + } else { + info.ParamOverrideAudit = nil + } + } return result, nil } +func shouldEnableParamOverrideAudit(paramOverride map[string]interface{}) bool { + if common.DebugEnabled { + return true + } + if len(paramOverride) == 0 { + return false + } + if operations, ok := tryParseOperations(paramOverride); ok { + for _, operation := range operations { + if shouldAuditParamPath(strings.TrimSpace(operation.Path)) || + shouldAuditParamPath(strings.TrimSpace(operation.To)) { + return true + } + } + for key := range buildLegacyParamOverride(paramOverride) { + if shouldAuditParamPath(strings.TrimSpace(key)) { + return true + } + } + return false + } + for key := range paramOverride { + if shouldAuditParamPath(strings.TrimSpace(key)) { + return true + } + } + return false +} + +func getParamOverrideAuditRecorder(context map[string]interface{}) *paramOverrideAuditRecorder { + if context == nil { + return nil + } + recorder, _ := context[paramOverrideContextAuditRecorder].(*paramOverrideAuditRecorder) + return recorder +} + +func (r *paramOverrideAuditRecorder) recordOperation(mode, path, from, to string, value interface{}) { + if r == nil { + return + } + line := buildParamOverrideAuditLine(mode, path, from, to, value) + if line == "" { + return + } + if lo.Contains(r.lines, line) { + return + } + r.lines = append(r.lines, line) +} + +func shouldAuditParamPath(path string) bool { + path = strings.TrimSpace(path) + if path == "" { + return false + } + if common.DebugEnabled { + return true + } + _, ok := paramOverrideKeyAuditPaths[path] + return ok +} + +func shouldAuditOperation(mode, path, from, to string) bool { + if common.DebugEnabled { + return true + } + for _, candidate := range []string{path, to} { + if shouldAuditParamPath(candidate) { + return true + } + } + return false +} + +func formatParamOverrideAuditValue(value interface{}) string { + switch typed := value.(type) { + case nil: + return "" + case string: + return typed + default: + return common.GetJsonString(typed) + } +} + +func buildParamOverrideAuditLine(mode, path, from, to string, value interface{}) string { + mode = strings.TrimSpace(mode) + path = strings.TrimSpace(path) + from = strings.TrimSpace(from) + to = strings.TrimSpace(to) + + if !shouldAuditOperation(mode, path, from, to) { + return "" + } + + switch mode { + case "set": + if path == "" { + return "" + } + return fmt.Sprintf("set %s = %s", path, formatParamOverrideAuditValue(value)) + case "delete": + if path == "" { + return "" + } + return fmt.Sprintf("delete %s", path) + case "copy": + if from == "" || to == "" { + return "" + } + return fmt.Sprintf("copy %s -> %s", from, to) + case "move": + if from == "" || to == "" { + return "" + } + return fmt.Sprintf("move %s -> %s", from, to) + case "prepend": + if path == "" { + return "" + } + return fmt.Sprintf("prepend %s with %s", path, formatParamOverrideAuditValue(value)) + case "append": + if path == "" { + return "" + } + return fmt.Sprintf("append %s with %s", path, formatParamOverrideAuditValue(value)) + case "trim_prefix", "trim_suffix", "ensure_prefix", "ensure_suffix": + if path == "" { + return "" + } + return fmt.Sprintf("%s %s with %s", mode, path, formatParamOverrideAuditValue(value)) + case "trim_space", "to_lower", "to_upper": + if path == "" { + return "" + } + return fmt.Sprintf("%s %s", mode, path) + case "replace", "regex_replace": + if path == "" { + return "" + } + return fmt.Sprintf("%s %s from %s to %s", mode, path, from, to) + case "set_header": + if path == "" { + return "" + } + return fmt.Sprintf("set_header %s = %s", path, formatParamOverrideAuditValue(value)) + case "delete_header": + if path == "" { + return "" + } + return fmt.Sprintf("delete_header %s", path) + case "copy_header", "move_header": + if from == "" || to == "" { + return "" + } + return fmt.Sprintf("%s %s -> %s", mode, from, to) + case "pass_headers": + return fmt.Sprintf("pass_headers %s", formatParamOverrideAuditValue(value)) + case "sync_fields": + if from == "" || to == "" { + return "" + } + return fmt.Sprintf("sync_fields %s -> %s", from, to) + case "return_error": + return fmt.Sprintf("return_error %s", formatParamOverrideAuditValue(value)) + default: + if path == "" { + return mode + } + return fmt.Sprintf("%s %s", mode, path) + } +} + func getParamOverrideMap(info *RelayInfo) map[string]interface{} { if info == nil || info.ChannelMeta == nil { return nil @@ -455,7 +655,7 @@ func compareNumeric(jsonValue, targetValue gjson.Result, operator string) (bool, } // applyOperationsLegacy 原参数覆盖方法 -func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}) ([]byte, error) { +func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}, auditRecorder *paramOverrideAuditRecorder) ([]byte, error) { reqMap := make(map[string]interface{}) err := common.Unmarshal(jsonData, &reqMap) if err != nil { @@ -464,6 +664,7 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{} for key, value := range paramOverride { reqMap[key] = value + auditRecorder.recordOperation("set", key, "", "", value) } return common.Marshal(reqMap) @@ -471,6 +672,7 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{} func applyOperations(jsonStr string, operations []ParamOperation, conditionContext map[string]interface{}) (string, error) { context := ensureContextMap(conditionContext) + auditRecorder := getParamOverrideAuditRecorder(context) contextJSON, err := marshalContextJSON(context) if err != nil { return "", fmt.Errorf("failed to marshal condition context: %v", err) @@ -506,6 +708,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("delete", path, "", "", nil) } case "set": for _, path := range opPaths { @@ -516,11 +719,15 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("set", path, "", "", op.Value) } case "move": opFrom := processNegativeIndex(result, op.From) opTo := processNegativeIndex(result, op.To) result, err = moveValue(result, opFrom, opTo) + if err == nil { + auditRecorder.recordOperation("move", "", opFrom, opTo, nil) + } case "copy": if op.From == "" || op.To == "" { return "", fmt.Errorf("copy from/to is required") @@ -528,12 +735,16 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte opFrom := processNegativeIndex(result, op.From) opTo := processNegativeIndex(result, op.To) result, err = copyValue(result, opFrom, opTo) + if err == nil { + auditRecorder.recordOperation("copy", "", opFrom, opTo, nil) + } case "prepend": for _, path := range opPaths { result, err = modifyValue(result, path, op.Value, op.KeepOrigin, true) if err != nil { break } + auditRecorder.recordOperation("prepend", path, "", "", op.Value) } case "append": for _, path := range opPaths { @@ -541,6 +752,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("append", path, "", "", op.Value) } case "trim_prefix": for _, path := range opPaths { @@ -548,6 +760,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("trim_prefix", path, "", "", op.Value) } case "trim_suffix": for _, path := range opPaths { @@ -555,6 +768,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("trim_suffix", path, "", "", op.Value) } case "ensure_prefix": for _, path := range opPaths { @@ -562,6 +776,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("ensure_prefix", path, "", "", op.Value) } case "ensure_suffix": for _, path := range opPaths { @@ -569,6 +784,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("ensure_suffix", path, "", "", op.Value) } case "trim_space": for _, path := range opPaths { @@ -576,6 +792,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("trim_space", path, "", "", nil) } case "to_lower": for _, path := range opPaths { @@ -583,6 +800,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("to_lower", path, "", "", nil) } case "to_upper": for _, path := range opPaths { @@ -590,6 +808,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("to_upper", path, "", "", nil) } case "replace": for _, path := range opPaths { @@ -597,6 +816,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("replace", path, op.From, op.To, nil) } case "regex_replace": for _, path := range opPaths { @@ -604,8 +824,10 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err != nil { break } + auditRecorder.recordOperation("regex_replace", path, op.From, op.To, nil) } case "return_error": + auditRecorder.recordOperation("return_error", op.Path, "", "", op.Value) returnErr, parseErr := parseParamOverrideReturnError(op.Value) if parseErr != nil { return "", parseErr @@ -621,11 +843,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte case "set_header": err = setHeaderOverrideInContext(context, op.Path, op.Value, op.KeepOrigin) if err == nil { + auditRecorder.recordOperation("set_header", op.Path, "", "", op.Value) contextJSON, err = marshalContextJSON(context) } case "delete_header": err = deleteHeaderOverrideInContext(context, op.Path) if err == nil { + auditRecorder.recordOperation("delete_header", op.Path, "", "", nil) contextJSON, err = marshalContextJSON(context) } case "copy_header": @@ -642,6 +866,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte err = nil } if err == nil { + auditRecorder.recordOperation("copy_header", "", sourceHeader, targetHeader, nil) contextJSON, err = marshalContextJSON(context) } case "move_header": @@ -658,6 +883,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte err = nil } if err == nil { + auditRecorder.recordOperation("move_header", "", sourceHeader, targetHeader, nil) contextJSON, err = marshalContextJSON(context) } case "pass_headers": @@ -675,11 +901,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte } } if err == nil { + auditRecorder.recordOperation("pass_headers", "", "", "", headerNames) contextJSON, err = marshalContextJSON(context) } case "sync_fields": result, err = syncFieldsBetweenTargets(result, context, op.From, op.To) if err == nil { + auditRecorder.recordOperation("sync_fields", "", op.From, op.To, nil) contextJSON, err = marshalContextJSON(context) } default: diff --git a/relay/common/override_test.go b/relay/common/override_test.go index c41be219..1a7793ba 100644 --- a/relay/common/override_test.go +++ b/relay/common/override_test.go @@ -6,6 +6,7 @@ import ( "reflect" "testing" + common2 "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/types" "github.com/QuantumNous/new-api/dto" @@ -2066,6 +2067,105 @@ func TestRemoveDisabledFieldsAllowInferenceGeo(t *testing.T) { assertJSONEqual(t, `{"inference_geo":"eu","store":true}`, string(out)) } +func TestApplyParamOverrideWithRelayInfoRecordsOperationAuditInDebugMode(t *testing.T) { + originalDebugEnabled := common2.DebugEnabled + common2.DebugEnabled = true + t.Cleanup(func() { + common2.DebugEnabled = originalDebugEnabled + }) + + info := &RelayInfo{ + ChannelMeta: &ChannelMeta{ + ParamOverride: map[string]interface{}{ + "operations": []interface{}{ + map[string]interface{}{ + "mode": "copy", + "from": "metadata.target_model", + "to": "model", + }, + map[string]interface{}{ + "mode": "set", + "path": "service_tier", + "value": "flex", + }, + map[string]interface{}{ + "mode": "set", + "path": "temperature", + "value": 0.1, + }, + }, + }, + }, + } + + out, err := ApplyParamOverrideWithRelayInfo([]byte(`{ + "model":"gpt-4.1", + "temperature":0.7, + "metadata":{"target_model":"gpt-4.1-mini"} + }`), info) + if err != nil { + t.Fatalf("ApplyParamOverrideWithRelayInfo returned error: %v", err) + } + assertJSONEqual(t, `{ + "model":"gpt-4.1-mini", + "temperature":0.1, + "service_tier":"flex", + "metadata":{"target_model":"gpt-4.1-mini"} + }`, string(out)) + + expected := []string{ + "copy metadata.target_model -> model", + "set service_tier = flex", + "set temperature = 0.1", + } + if !reflect.DeepEqual(info.ParamOverrideAudit, expected) { + t.Fatalf("unexpected param override audit, got %#v", info.ParamOverrideAudit) + } +} + +func TestApplyParamOverrideWithRelayInfoRecordsOnlyKeyOperationsWhenDebugDisabled(t *testing.T) { + originalDebugEnabled := common2.DebugEnabled + common2.DebugEnabled = false + t.Cleanup(func() { + common2.DebugEnabled = originalDebugEnabled + }) + + info := &RelayInfo{ + ChannelMeta: &ChannelMeta{ + ParamOverride: map[string]interface{}{ + "operations": []interface{}{ + map[string]interface{}{ + "mode": "copy", + "from": "metadata.target_model", + "to": "model", + }, + map[string]interface{}{ + "mode": "set", + "path": "temperature", + "value": 0.1, + }, + }, + }, + }, + } + + _, err := ApplyParamOverrideWithRelayInfo([]byte(`{ + "model":"gpt-4.1", + "temperature":0.7, + "metadata":{"target_model":"gpt-4.1-mini"} + }`), info) + if err != nil { + t.Fatalf("ApplyParamOverrideWithRelayInfo returned error: %v", err) + } + + expected := []string{ + "copy metadata.target_model -> model", + } + if !reflect.DeepEqual(info.ParamOverrideAudit, expected) { + t.Fatalf("unexpected param override audit, got %#v", info.ParamOverrideAudit) + } +} + func assertJSONEqual(t *testing.T, want, got string) { t.Helper() diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 8b0789c0..c90ecb44 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -149,6 +149,7 @@ type RelayInfo struct { LastError *types.NewAPIError RuntimeHeadersOverride map[string]interface{} UseRuntimeHeadersOverride bool + ParamOverrideAudit []string PriceData types.PriceData diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 1c440911..eea2ea07 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -74,9 +74,17 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m appendRequestPath(ctx, relayInfo, other) appendRequestConversionChain(relayInfo, other) appendBillingInfo(relayInfo, other) + appendParamOverrideInfo(relayInfo, other) return other } +func appendParamOverrideInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { + if relayInfo == nil || other == nil || len(relayInfo.ParamOverrideAudit) == 0 { + return + } + other["po"] = relayInfo.ParamOverrideAudit +} + func appendBillingInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { if relayInfo == nil || other == nil { return diff --git a/web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx b/web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx new file mode 100644 index 00000000..aa7c7499 --- /dev/null +++ b/web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx @@ -0,0 +1,54 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Typography } from '@douyinfe/semi-ui'; + +const { Text } = Typography; + +const ParamOverrideEntry = ({ count, onOpen, t }) => { + return ( +
+ + {t('{{count}} 项操作', { count })} + + + {t('查看详情')} + +
+ ); +}; + +export default React.memo(ParamOverrideEntry); diff --git a/web/src/components/table/usage-logs/index.jsx b/web/src/components/table/usage-logs/index.jsx index 7d2d47c3..ce5a17f8 100644 --- a/web/src/components/table/usage-logs/index.jsx +++ b/web/src/components/table/usage-logs/index.jsx @@ -25,6 +25,7 @@ import LogsFilters from './UsageLogsFilters'; import ColumnSelectorModal from './modals/ColumnSelectorModal'; import UserInfoModal from './modals/UserInfoModal'; import ChannelAffinityUsageCacheModal from './modals/ChannelAffinityUsageCacheModal'; +import ParamOverrideModal from './modals/ParamOverrideModal'; import { useLogsData } from '../../../hooks/usage-logs/useUsageLogsData'; import { useIsMobile } from '../../../hooks/common/useIsMobile'; import { createCardProPagination } from '../../../helpers/utils'; @@ -39,6 +40,7 @@ const LogsPage = () => { + {/* Main Content */} . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useMemo } from 'react'; +import { + Modal, + Button, + Empty, + Divider, + Typography, +} from '@douyinfe/semi-ui'; +import { IconCopy } from '@douyinfe/semi-icons'; +import { copy, showError, showSuccess } from '../../../../helpers'; + +const { Text } = Typography; + +const parseAuditLine = (line) => { + if (typeof line !== 'string') { + return null; + } + const firstSpaceIndex = line.indexOf(' '); + if (firstSpaceIndex <= 0) { + return { action: line, content: line }; + } + return { + action: line.slice(0, firstSpaceIndex), + content: line.slice(firstSpaceIndex + 1), + }; +}; + +const getActionLabel = (action, t) => { + switch ((action || '').toLowerCase()) { + case 'set': + return t('设置'); + case 'delete': + return t('删除'); + case 'copy': + return t('复制'); + case 'move': + return t('移动'); + case 'append': + return t('追加'); + case 'prepend': + return t('前置'); + case 'trim_prefix': + return t('去前缀'); + case 'trim_suffix': + return t('去后缀'); + case 'ensure_prefix': + return t('保前缀'); + case 'ensure_suffix': + return t('保后缀'); + case 'trim_space': + return t('去空格'); + case 'to_lower': + return t('转小写'); + case 'to_upper': + return t('转大写'); + case 'replace': + return t('替换'); + case 'regex_replace': + return t('正则替换'); + case 'set_header': + return t('设请求头'); + case 'delete_header': + return t('删请求头'); + case 'copy_header': + return t('复制请求头'); + case 'move_header': + return t('移动请求头'); + case 'pass_headers': + return t('透传请求头'); + case 'sync_fields': + return t('同步字段'); + case 'return_error': + return t('返回错误'); + default: + return action; + } +}; + +const ParamOverrideModal = ({ + showParamOverrideModal, + setShowParamOverrideModal, + paramOverrideTarget, + t, +}) => { + const lines = Array.isArray(paramOverrideTarget?.lines) + ? paramOverrideTarget.lines + : []; + + const parsedLines = useMemo(() => { + return lines.map(parseAuditLine); + }, [lines]); + + const copyAll = async () => { + const content = lines.join('\n'); + if (!content) { + return; + } + if (await copy(content)) { + showSuccess(t('参数覆盖已复制')); + return; + } + showError(t('无法复制到剪贴板,请手动复制')); + }; + + return ( + setShowParamOverrideModal(false)} + footer={null} + centered + closable + maskClosable + width={640} + > +
+
+
+
+ + {t('{{count}} 项操作', { count: lines.length })} + +
+
+ {paramOverrideTarget?.modelName ? ( + + {paramOverrideTarget.modelName} + + ) : null} + {paramOverrideTarget?.requestId ? ( + + {t('Request ID')}: {paramOverrideTarget.requestId} + + ) : null} + {paramOverrideTarget?.requestPath ? ( + + {t('请求路径')}: {paramOverrideTarget.requestPath} + + ) : null} +
+
+ + +
+ + + + {lines.length === 0 ? ( + + ) : ( +
+ {parsedLines.map((item, index) => { + if (!item) { + return null; + } + + return ( +
+
+ + {getActionLabel(item.action, t)} + +
+ + {item.content} + +
+ ); + })} +
+ )} +
+
+ ); +}; + +export default ParamOverrideModal; diff --git a/web/src/hooks/usage-logs/useUsageLogsData.jsx b/web/src/hooks/usage-logs/useUsageLogsData.jsx index ebe1b882..d4ac9df4 100644 --- a/web/src/hooks/usage-logs/useUsageLogsData.jsx +++ b/web/src/hooks/usage-logs/useUsageLogsData.jsx @@ -39,6 +39,7 @@ import { } from '../../helpers'; import { ITEMS_PER_PAGE } from '../../constants'; import { useTableCompactMode } from '../common/useTableCompactMode'; +import ParamOverrideEntry from '../../components/table/usage-logs/components/ParamOverrideEntry'; export const useLogsData = () => { const { t } = useTranslation(); @@ -181,6 +182,8 @@ export const useLogsData = () => { ] = useState(false); const [channelAffinityUsageCacheTarget, setChannelAffinityUsageCacheTarget] = useState(null); + const [showParamOverrideModal, setShowParamOverrideModal] = useState(false); + const [paramOverrideTarget, setParamOverrideTarget] = useState(null); // Initialize default column visibility const initDefaultColumns = () => { @@ -345,6 +348,20 @@ export const useLogsData = () => { setShowChannelAffinityUsageCacheModal(true); }; + const openParamOverrideModal = (log, other) => { + const lines = Array.isArray(other?.po) ? other.po.filter(Boolean) : []; + if (lines.length === 0) { + return; + } + setParamOverrideTarget({ + lines, + modelName: log?.model_name || '', + requestId: log?.request_id || '', + requestPath: other?.request_path || '', + }); + setShowParamOverrideModal(true); + }; + // Format logs data const setLogsFormat = (logs) => { const requestConversionDisplayValue = (conversionChain) => { @@ -584,6 +601,21 @@ export const useLogsData = () => { value: other.request_path, }); } + if (Array.isArray(other?.po) && other.po.length > 0) { + expandDataLocal.push({ + key: t('参数覆盖'), + value: ( + { + event.stopPropagation(); + openParamOverrideModal(logs[i], other); + }} + /> + ), + }); + } if (other?.billing_source === 'subscription') { const planId = other?.subscription_plan_id; const planTitle = other?.subscription_plan_title || ''; @@ -811,6 +843,9 @@ export const useLogsData = () => { setShowChannelAffinityUsageCacheModal, channelAffinityUsageCacheTarget, openChannelAffinityUsageCacheModal, + showParamOverrideModal, + setShowParamOverrideModal, + paramOverrideTarget, // Functions loadLogs, @@ -822,6 +857,7 @@ export const useLogsData = () => { setLogsFormat, hasExpandableRows, setLogType, + openParamOverrideModal, // Translation t,