diff --git a/relay/common/override.go b/relay/common/override.go index 558fd55a..b1a7d177 100644 --- a/relay/common/override.go +++ b/relay/common/override.go @@ -2094,6 +2094,10 @@ func mergeObjects(data []byte, path string, value interface{}, keepOrigin bool) // 目前内置以下字段: // - upstream_model/model:始终为通道映射后的上游模型名。 // - original_model:请求最初指定的模型名。 +// - user_id:已认证用户 ID。 +// - user_group:用户所属分组。 +// - token_group:令牌指定的分组;未指定时回退为用户分组。 +// - using_group:当前实际使用的分组,自动跨分组重试时可能变化。 // - request_path:请求路径 // - is_channel_test:是否为渠道测试请求(同 is_test)。 func BuildParamOverrideContext(info *RelayInfo) map[string]interface{} { @@ -2102,6 +2106,10 @@ func BuildParamOverrideContext(info *RelayInfo) map[string]interface{} { } ctx := make(map[string]interface{}) + ctx["user_id"] = info.UserId + ctx["user_group"] = info.UserGroup + ctx["token_group"] = info.TokenGroup + ctx["using_group"] = info.UsingGroup if info.ChannelMeta != nil && info.ChannelMeta.UpstreamModelName != "" { ctx["model"] = info.ChannelMeta.UpstreamModelName ctx["upstream_model"] = info.ChannelMeta.UpstreamModelName diff --git a/relay/common/override_test.go b/relay/common/override_test.go index e1f84eaf..19af348d 100644 --- a/relay/common/override_test.go +++ b/relay/common/override_test.go @@ -1260,6 +1260,116 @@ func TestApplyParamOverrideConditionFromRetryAndLastErrorContext(t *testing.T) { assertJSONEqual(t, `{"temperature":0.1}`, string(out)) } +func TestApplyParamOverrideConditionByUserAndGPTModel(t *testing.T) { + paramOverride := map[string]interface{}{ + "operations": []interface{}{ + map[string]interface{}{ + "path": "service_tier", + "mode": "set", + "value": "priority", + "logic": "AND", + "conditions": []interface{}{ + map[string]interface{}{ + "path": "user_id", + "mode": "full", + "value": 1, + }, + map[string]interface{}{ + "path": "upstream_model", + "mode": "contains", + "value": "gpt", + }, + }, + }, + }, + } + + tests := []struct { + name string + userID int + model string + expected string + }{ + { + name: "target user and GPT model", + userID: 1, + model: "gpt-5.2", + expected: `{"model":"gpt-5.2","service_tier":"priority"}`, + }, + { + name: "other user", + userID: 2, + model: "gpt-5.2", + expected: `{"model":"gpt-5.2"}`, + }, + { + name: "non-GPT model", + userID: 1, + model: "claude-sonnet-4-5", + expected: `{"model":"claude-sonnet-4-5"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + info := &RelayInfo{ + UserId: tt.userID, + ChannelMeta: &ChannelMeta{ + ParamOverride: paramOverride, + UpstreamModelName: tt.model, + }, + } + input := []byte(fmt.Sprintf(`{"model":%q}`, tt.model)) + + out, err := ApplyParamOverrideWithRelayInfo(input, info) + + require.NoError(t, err) + require.JSONEq(t, tt.expected, string(out)) + }) + } +} + +func TestApplyParamOverrideConditionByGroupContext(t *testing.T) { + info := &RelayInfo{ + UserGroup: "vip", + TokenGroup: "premium", + UsingGroup: "priority-route", + } + ctx := BuildParamOverrideContext(info) + paramOverride := map[string]interface{}{ + "operations": []interface{}{ + map[string]interface{}{ + "path": "service_tier", + "mode": "set", + "value": "priority", + "logic": "AND", + "conditions": []interface{}{ + map[string]interface{}{ + "path": "user_group", + "mode": "full", + "value": "vip", + }, + map[string]interface{}{ + "path": "token_group", + "mode": "full", + "value": "premium", + }, + map[string]interface{}{ + "path": "using_group", + "mode": "full", + "value": "priority-route", + }, + }, + }, + }, + } + + out, err := ApplyParamOverride([]byte(`{"model":"gpt-5.2"}`), paramOverride, ctx) + + require.NoError(t, err) + require.JSONEq(t, `{"model":"gpt-5.2","service_tier":"priority"}`, string(out)) +} + func TestApplyParamOverrideConditionFromRequestHeaders(t *testing.T) { input := []byte(`{"temperature":0.7}`) override := map[string]interface{}{