mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-29 08:11:32 +00:00
feat(groups): scope OpenAI reasoning effort mappings by model
Allow OpenAI/Composite group mappings to match by exact model name, prefix, or suffix. Empty type and model still apply to every model. The admin form groups multiple request-to-forward pairs under one type+model scope.
This commit is contained in:
@@ -274,7 +274,7 @@ func (Group) Fields() []ent.Field {
|
||||
field.JSON("reasoning_effort_mappings", []domain.ReasoningEffortMapping{}).
|
||||
Default([]domain.ReasoningEffortMapping{}).
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
|
||||
Comment("OpenAI reasoning effort 自定义精确映射;先映射再应用上限"),
|
||||
Comment("OpenAI reasoning effort 自定义映射;可按模型精确名、前缀或后缀限定,先映射再应用上限"),
|
||||
|
||||
// 分组利润控制(migration 192/193):openai/anthropic/gemini/grok/antigravity
|
||||
// 的 token 分组可启用,composite 分组不能直接启用。
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
package domain
|
||||
|
||||
const (
|
||||
ReasoningEffortMatchExact = "exact"
|
||||
ReasoningEffortMatchPrefix = "prefix"
|
||||
ReasoningEffortMatchSuffix = "suffix"
|
||||
)
|
||||
|
||||
// ReasoningEffortMapping rewrites one explicit OpenAI/Codex reasoning effort
|
||||
// value to another before the group ceiling is applied.
|
||||
//
|
||||
// Model and MatchType optionally scope the rewrite to a request model:
|
||||
// exact matches the full model id, prefix/suffix match a model-id affix.
|
||||
// Empty MatchType and empty Model mean the mapping applies to every model.
|
||||
type ReasoningEffortMapping struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
MatchType string `json:"match_type,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ type CreateGroupRequest struct {
|
||||
RPMLimit int `json:"rpm_limit"`
|
||||
// OpenAI/Codex 请求推理强度上限,空字符串表示不限制。
|
||||
MaxReasoningEffort string `json:"max_reasoning_effort"`
|
||||
// OpenAI/Codex 推理强度精确映射。
|
||||
// OpenAI/Codex 推理强度映射,可按模型精确名、前缀或后缀限定。
|
||||
ReasoningEffortMappings []service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
|
||||
// 从指定分组复制账号(创建后自动绑定)
|
||||
CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"`
|
||||
|
||||
@@ -29,4 +29,15 @@ func TestUpdateGroupRequestReasoningEffortMappingsTriState(t *testing.T) {
|
||||
require.Equal(t, "max", (*req.ReasoningEffortMappings)[0].From)
|
||||
require.Equal(t, "xhigh", (*req.ReasoningEffortMappings)[0].To)
|
||||
})
|
||||
|
||||
t.Run("accepts model scoped mappings", func(t *testing.T) {
|
||||
var req UpdateGroupRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"reasoning_effort_mappings":[{"from":"max","to":"low","match_type":"prefix","model":"gpt"}]}`), &req))
|
||||
require.NotNil(t, req.ReasoningEffortMappings)
|
||||
require.Len(t, *req.ReasoningEffortMappings, 1)
|
||||
require.Equal(t, "max", (*req.ReasoningEffortMappings)[0].From)
|
||||
require.Equal(t, "low", (*req.ReasoningEffortMappings)[0].To)
|
||||
require.Equal(t, "prefix", (*req.ReasoningEffortMappings)[0].MatchType)
|
||||
require.Equal(t, "gpt", (*req.ReasoningEffortMappings)[0].Model)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ type Group struct {
|
||||
RPMLimit int `json:"rpm_limit"`
|
||||
// MaxReasoningEffort OpenAI/Codex 请求的推理强度上限,空字符串表示不限制。
|
||||
MaxReasoningEffort string `json:"max_reasoning_effort"`
|
||||
// ReasoningEffortMappings OpenAI/Codex 推理强度精确映射。
|
||||
// ReasoningEffortMappings OpenAI/Codex 推理强度映射,可按模型精确名、前缀或后缀限定。
|
||||
ReasoningEffortMappings []domain.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
@@ -886,6 +886,20 @@ func TestAdminService_UpdateGroup_ReasoningEffortMappingsTriState(t *testing.T)
|
||||
}(),
|
||||
want: []ReasoningEffortMapping{{From: "xhigh", To: "high"}},
|
||||
},
|
||||
{
|
||||
name: "model scoped mappings are canonicalized independently",
|
||||
input: func() *UpdateGroupInput {
|
||||
replacement := []ReasoningEffortMapping{
|
||||
{From: " MAX ", To: " low ", MatchType: "PREFIX", Model: " gpt "},
|
||||
{From: "max", To: "medium", Model: "gpt-5.4"},
|
||||
}
|
||||
return &UpdateGroupInput{ReasoningEffortMappings: &replacement}
|
||||
}(),
|
||||
want: []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: "prefix", Model: "gpt"},
|
||||
{From: "max", To: "medium", MatchType: "exact", Model: "gpt-5.4"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
const (
|
||||
maxReasoningEffortMappings = 64
|
||||
maxReasoningEffortValueLen = 64
|
||||
maxReasoningEffortModelLen = 200
|
||||
)
|
||||
|
||||
var openAIReasoningEffortValues = []string{"minimal", "low", "medium", "high", "xhigh", "max"}
|
||||
@@ -103,8 +105,109 @@ func reasoningEffortRank(raw string) (int, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeReasoningEffortMatchType(matchType, model string) (string, error) {
|
||||
model = strings.TrimSpace(model)
|
||||
matchType = strings.ToLower(strings.TrimSpace(matchType))
|
||||
if model == "" {
|
||||
return "", nil
|
||||
}
|
||||
switch matchType {
|
||||
case "", domain.ReasoningEffortMatchExact:
|
||||
return domain.ReasoningEffortMatchExact, nil
|
||||
case domain.ReasoningEffortMatchPrefix:
|
||||
return domain.ReasoningEffortMatchPrefix, nil
|
||||
case domain.ReasoningEffortMatchSuffix:
|
||||
return domain.ReasoningEffortMatchSuffix, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid match_type %q", matchType)
|
||||
}
|
||||
}
|
||||
|
||||
func reasoningEffortMappingDuplicateKey(from, matchType, model string) string {
|
||||
return from + "\x00" + matchType + "\x00" + strings.ToLower(strings.TrimSpace(model))
|
||||
}
|
||||
|
||||
func requestModelMatchesReasoningEffortMapping(requestModel, matchType, mappingModel string) bool {
|
||||
scope := strings.ToLower(strings.TrimSpace(mappingModel))
|
||||
req := strings.ToLower(strings.TrimSpace(requestModel))
|
||||
switch matchType {
|
||||
case "":
|
||||
return true
|
||||
case domain.ReasoningEffortMatchExact:
|
||||
return scope != "" && scope == req
|
||||
case domain.ReasoningEffortMatchPrefix:
|
||||
return scope != "" && req != "" && strings.HasPrefix(req, scope)
|
||||
case domain.ReasoningEffortMatchSuffix:
|
||||
return scope != "" && req != "" && strings.HasSuffix(req, scope)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func selectReasoningEffortMapping(mappings []ReasoningEffortMapping, from, requestModel string) (ReasoningEffortMapping, bool) {
|
||||
type candidate struct {
|
||||
mapping ReasoningEffortMapping
|
||||
matchStrength int
|
||||
patternLen int
|
||||
index int
|
||||
}
|
||||
candidates := make([]candidate, 0, len(mappings))
|
||||
for i, mapping := range mappings {
|
||||
if NormalizeMaxReasoningEffort(mapping.From) != from {
|
||||
continue
|
||||
}
|
||||
model := strings.TrimSpace(mapping.Model)
|
||||
matchType, err := normalizeReasoningEffortMatchType(mapping.MatchType, model)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !requestModelMatchesReasoningEffortMapping(requestModel, matchType, model) {
|
||||
continue
|
||||
}
|
||||
strength := 1
|
||||
patternLen := 0
|
||||
switch matchType {
|
||||
case domain.ReasoningEffortMatchExact:
|
||||
strength = 3
|
||||
case domain.ReasoningEffortMatchPrefix, domain.ReasoningEffortMatchSuffix:
|
||||
strength = 2
|
||||
patternLen = len(strings.ToLower(model))
|
||||
}
|
||||
candidates = append(candidates, candidate{
|
||||
mapping: mapping,
|
||||
matchStrength: strength,
|
||||
patternLen: patternLen,
|
||||
index: i,
|
||||
})
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return ReasoningEffortMapping{}, false
|
||||
}
|
||||
best := candidates[0]
|
||||
for _, item := range candidates[1:] {
|
||||
if item.matchStrength != best.matchStrength {
|
||||
if item.matchStrength > best.matchStrength {
|
||||
best = item
|
||||
}
|
||||
continue
|
||||
}
|
||||
if item.patternLen != best.patternLen {
|
||||
if item.patternLen > best.patternLen {
|
||||
best = item
|
||||
}
|
||||
continue
|
||||
}
|
||||
if item.index < best.index {
|
||||
best = item
|
||||
}
|
||||
}
|
||||
return best.mapping, true
|
||||
}
|
||||
|
||||
// NormalizeReasoningEffortMappings validates group mapping rules against the
|
||||
// fixed effort values supported by OpenAI routes.
|
||||
// fixed effort values supported by OpenAI routes. Optional model scopes are
|
||||
// canonicalized to exact/prefix/suffix match; empty type and model keep the
|
||||
// global all-models rule.
|
||||
func NormalizeReasoningEffortMappings(platform string, raw []ReasoningEffortMapping) ([]ReasoningEffortMapping, error) {
|
||||
if len(raw) > maxReasoningEffortMappings {
|
||||
return nil, fmt.Errorf("reasoning effort mappings cannot exceed %d entries", maxReasoningEffortMappings)
|
||||
@@ -127,12 +230,28 @@ func NormalizeReasoningEffortMappings(platform string, raw []ReasoningEffortMapp
|
||||
if _, err := normalizeMaxReasoningEffortForPlatform(platform, to); err != nil {
|
||||
return nil, fmt.Errorf("reasoning effort mapping %d target: %w", i+1, err)
|
||||
}
|
||||
key := from
|
||||
model := strings.TrimSpace(mapping.Model)
|
||||
if len(model) > maxReasoningEffortModelLen {
|
||||
return nil, fmt.Errorf("reasoning effort mapping %d model cannot exceed %d characters", i+1, maxReasoningEffortModelLen)
|
||||
}
|
||||
matchType, err := normalizeReasoningEffortMatchType(mapping.MatchType, model)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reasoning effort mapping %d: %w", i+1, err)
|
||||
}
|
||||
key := reasoningEffortMappingDuplicateKey(from, matchType, model)
|
||||
if _, exists := seen[key]; exists {
|
||||
return nil, fmt.Errorf("duplicate reasoning effort mapping source %q", from)
|
||||
if model == "" {
|
||||
return nil, fmt.Errorf("duplicate reasoning effort mapping source %q", from)
|
||||
}
|
||||
return nil, fmt.Errorf("duplicate reasoning effort mapping source %q for %s model %q", from, matchType, strings.ToLower(model))
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
normalized = append(normalized, ReasoningEffortMapping{From: from, To: to})
|
||||
normalized = append(normalized, ReasoningEffortMapping{
|
||||
From: from,
|
||||
To: to,
|
||||
MatchType: matchType,
|
||||
Model: model,
|
||||
})
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
@@ -194,15 +313,17 @@ func ApplyOpenAIReasoningEffortPolicyFromContext(ctx context.Context, body []byt
|
||||
return ApplyOpenAIReasoningEffortPolicy(body, policy.maxEffort, policy.mappings)
|
||||
}
|
||||
|
||||
func mapReasoningEffort(raw string, mappings []ReasoningEffortMapping) (string, bool) {
|
||||
func mapReasoningEffort(raw string, mappings []ReasoningEffortMapping, requestModel string) (string, bool) {
|
||||
value := strings.TrimSpace(raw)
|
||||
canonical := NormalizeMaxReasoningEffort(value)
|
||||
for _, mapping := range mappings {
|
||||
if canonical != "" && canonical == NormalizeMaxReasoningEffort(mapping.From) {
|
||||
return strings.TrimSpace(mapping.To), true
|
||||
}
|
||||
if canonical == "" {
|
||||
return value, false
|
||||
}
|
||||
return value, false
|
||||
mapping, ok := selectReasoningEffortMapping(mappings, canonical, requestModel)
|
||||
if !ok {
|
||||
return value, false
|
||||
}
|
||||
return strings.TrimSpace(mapping.To), true
|
||||
}
|
||||
|
||||
func sanitizeGroupReasoningEffortPolicy(group *Group) {
|
||||
@@ -221,15 +342,17 @@ func sanitizeGroupReasoningEffortPolicy(group *Group) {
|
||||
group.ReasoningEffortMappings = mappings
|
||||
}
|
||||
|
||||
// ApplyOpenAIReasoningEffortPolicy applies one exact mapping and then caps
|
||||
// known effort levels. Omitted values remain untouched so upstream defaults
|
||||
// stay in control.
|
||||
// ApplyOpenAIReasoningEffortPolicy applies one mapping (optionally scoped to
|
||||
// the request model by exact name, prefix, or suffix) and then caps known
|
||||
// effort levels. Omitted values remain untouched so upstream defaults stay in
|
||||
// control.
|
||||
func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []ReasoningEffortMapping) ([]byte, bool) {
|
||||
maxRank, hasMax := reasoningEffortRank(maxEffort)
|
||||
if len(body) == 0 || (!hasMax && len(mappings) == 0) {
|
||||
return body, false
|
||||
}
|
||||
|
||||
requestModel := strings.TrimSpace(gjson.GetBytes(body, "model").String())
|
||||
result := body
|
||||
changed := false
|
||||
for _, path := range []string{"reasoning.effort", "reasoning_effort"} {
|
||||
@@ -242,7 +365,7 @@ func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []
|
||||
continue
|
||||
}
|
||||
|
||||
effective, _ := mapReasoningEffort(original, mappings)
|
||||
effective, _ := mapReasoningEffort(original, mappings, requestModel)
|
||||
if currentRank, recognized := reasoningEffortRank(effective); recognized {
|
||||
effective = NormalizeMaxReasoningEffort(effective)
|
||||
if hasMax && currentRank > maxRank {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
@@ -83,6 +84,68 @@ func TestNormalizeReasoningEffortMappings(t *testing.T) {
|
||||
require.ErrorContains(t, err, "duplicate")
|
||||
})
|
||||
|
||||
t.Run("allows same source across different model scopes", func(t *testing.T) {
|
||||
got, err := NormalizeReasoningEffortMappings(PlatformOpenAI, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: "prefix", Model: " gpt "},
|
||||
{From: "max", To: "medium", MatchType: "exact", Model: "GPT-5.4"},
|
||||
{From: "max", To: "high"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchPrefix, Model: "gpt"},
|
||||
{From: "max", To: "medium", MatchType: domain.ReasoningEffortMatchExact, Model: "GPT-5.4"},
|
||||
{From: "max", To: "high"},
|
||||
}, got)
|
||||
})
|
||||
|
||||
t.Run("defaults missing match type to exact when model is set", func(t *testing.T) {
|
||||
got, err := NormalizeReasoningEffortMappings(PlatformOpenAI, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", Model: "gpt-5.4"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchExact, Model: "gpt-5.4"},
|
||||
}, got)
|
||||
})
|
||||
|
||||
t.Run("empty type and model collapse to a global mapping", func(t *testing.T) {
|
||||
got, err := NormalizeReasoningEffortMappings(PlatformOpenAI, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: "prefix"},
|
||||
{From: "high", To: "low", MatchType: "suffix"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low"},
|
||||
{From: "high", To: "low"},
|
||||
}, got)
|
||||
})
|
||||
|
||||
t.Run("canonicalizes suffix match", func(t *testing.T) {
|
||||
got, err := NormalizeReasoningEffortMappings(PlatformOpenAI, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: " SUFFIX ", Model: " mini "},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchSuffix, Model: "mini"},
|
||||
}, got)
|
||||
})
|
||||
|
||||
t.Run("rejects invalid match type", func(t *testing.T) {
|
||||
_, err := NormalizeReasoningEffortMappings(PlatformOpenAI, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: "wildcard", Model: "gpt"},
|
||||
})
|
||||
require.ErrorContains(t, err, "invalid match_type")
|
||||
})
|
||||
|
||||
t.Run("rejects duplicate source within the same model scope", func(t *testing.T) {
|
||||
_, err := NormalizeReasoningEffortMappings(PlatformOpenAI, []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: "prefix", Model: "gpt"},
|
||||
{From: "MAX", To: "high", MatchType: "PREFIX", Model: " GPT "},
|
||||
})
|
||||
require.ErrorContains(t, err, "duplicate")
|
||||
require.ErrorContains(t, err, "gpt")
|
||||
})
|
||||
|
||||
t.Run("rejects mappings for non OpenAI platforms", func(t *testing.T) {
|
||||
for _, platform := range []string{PlatformAnthropic, PlatformGemini, PlatformAntigravity, PlatformGrok} {
|
||||
_, err := NormalizeReasoningEffortMappings(platform, []ReasoningEffortMapping{{From: "low", To: "high"}})
|
||||
@@ -152,6 +215,131 @@ func TestApplyOpenAIReasoningEffortPolicy(t *testing.T) {
|
||||
{name: "does not chain mappings", body: `{"reasoning_effort":"max"}`, mappings: []ReasoningEffortMapping{{From: "max", To: "xhigh"}, {From: "xhigh", To: "low"}}, path: "reasoning_effort", want: "xhigh", changed: true},
|
||||
{name: "keeps unknown without mapping", body: `{"reasoning_effort":"future"}`, max: "low", path: "reasoning_effort", want: "future", changed: false},
|
||||
{name: "keeps non string value", body: `{"reasoning_effort":{"level":"high"}}`, max: "low", path: "reasoning_effort.level", want: "high", changed: false},
|
||||
{
|
||||
name: "prefix mapping applies to matching model",
|
||||
body: `{"model":"gpt-5.4","reasoning_effort":"max"}`,
|
||||
mappings: []ReasoningEffortMapping{{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchPrefix, Model: "gpt"}},
|
||||
path: "reasoning_effort",
|
||||
want: "low",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "prefix mapping skips non matching model",
|
||||
body: `{"model":"o3","reasoning_effort":"max"}`,
|
||||
mappings: []ReasoningEffortMapping{{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchPrefix, Model: "gpt"}},
|
||||
path: "reasoning_effort",
|
||||
want: "max",
|
||||
changed: false,
|
||||
},
|
||||
{
|
||||
name: "exact mapping beats prefix",
|
||||
body: `{"model":"gpt-5.4","reasoning":{"effort":"max"}}`,
|
||||
mappings: []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchPrefix, Model: "gpt"},
|
||||
{From: "max", To: "medium", MatchType: domain.ReasoningEffortMatchExact, Model: "gpt-5.4"},
|
||||
},
|
||||
path: "reasoning.effort",
|
||||
want: "medium",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "longer prefix beats shorter prefix",
|
||||
body: `{"model":"gpt-5.4","reasoning_effort":"max"}`,
|
||||
mappings: []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchPrefix, Model: "gpt"},
|
||||
{From: "max", To: "high", MatchType: domain.ReasoningEffortMatchPrefix, Model: "gpt-5"},
|
||||
},
|
||||
path: "reasoning_effort",
|
||||
want: "high",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "falls back to global mapping when no model scope hits",
|
||||
body: `{"model":"o3","reasoning_effort":"max"}`,
|
||||
mappings: []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchPrefix, Model: "gpt"},
|
||||
{From: "max", To: "medium"},
|
||||
},
|
||||
path: "reasoning_effort",
|
||||
want: "medium",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "model scoped mapping still respects ceiling",
|
||||
body: `{"model":"gpt-5.4","reasoning":{"effort":"max"}}`,
|
||||
max: "medium",
|
||||
mappings: []ReasoningEffortMapping{
|
||||
{From: "max", To: "xhigh", MatchType: domain.ReasoningEffortMatchPrefix, Model: "gpt"},
|
||||
},
|
||||
path: "reasoning.effort",
|
||||
want: "medium",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "model match is case insensitive",
|
||||
body: `{"model":"GPT-5.4","reasoning_effort":"MAX"}`,
|
||||
mappings: []ReasoningEffortMapping{{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchExact, Model: "gpt-5.4"}},
|
||||
path: "reasoning_effort",
|
||||
want: "low",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "suffix mapping applies to matching model",
|
||||
body: `{"model":"gpt-5.4-mini","reasoning_effort":"max"}`,
|
||||
mappings: []ReasoningEffortMapping{{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchSuffix, Model: "mini"}},
|
||||
path: "reasoning_effort",
|
||||
want: "low",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "suffix mapping skips non matching model",
|
||||
body: `{"model":"gpt-5.4","reasoning_effort":"max"}`,
|
||||
mappings: []ReasoningEffortMapping{{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchSuffix, Model: "mini"}},
|
||||
path: "reasoning_effort",
|
||||
want: "max",
|
||||
changed: false,
|
||||
},
|
||||
{
|
||||
name: "exact mapping beats suffix",
|
||||
body: `{"model":"gpt-5.4-mini","reasoning":{"effort":"max"}}`,
|
||||
mappings: []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchSuffix, Model: "mini"},
|
||||
{From: "max", To: "medium", MatchType: domain.ReasoningEffortMatchExact, Model: "gpt-5.4-mini"},
|
||||
},
|
||||
path: "reasoning.effort",
|
||||
want: "medium",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "longer suffix beats shorter suffix",
|
||||
body: `{"model":"gpt-5.4-mini","reasoning_effort":"max"}`,
|
||||
mappings: []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchSuffix, Model: "i"},
|
||||
{From: "max", To: "high", MatchType: domain.ReasoningEffortMatchSuffix, Model: "mini"},
|
||||
},
|
||||
path: "reasoning_effort",
|
||||
want: "high",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "longer affix beats the other affix",
|
||||
body: `{"model":"gpt-5.4-mini","reasoning_effort":"max"}`,
|
||||
mappings: []ReasoningEffortMapping{
|
||||
{From: "max", To: "low", MatchType: domain.ReasoningEffortMatchSuffix, Model: "mini"},
|
||||
{From: "max", To: "high", MatchType: domain.ReasoningEffortMatchPrefix, Model: "gpt-5.4"},
|
||||
},
|
||||
path: "reasoning_effort",
|
||||
want: "high",
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "empty type and model apply to every model",
|
||||
body: `{"model":"o3","reasoning_effort":"max"}`,
|
||||
mappings: []ReasoningEffortMapping{{From: "max", To: "low"}},
|
||||
path: "reasoning_effort",
|
||||
want: "low",
|
||||
changed: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user