Files
team-api/relay/handler/relay_handler_test.go

120 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"context"
"strings"
"testing"
"github.com/qianfree/team-api/relay/common"
)
type modelMappingStub struct {
models map[string]bool
calls []string
}
func (s *modelMappingStub) GetModelMapping(_ context.Context, modelName string) (string, string, error) {
s.calls = append(s.calls, modelName)
if !s.models[modelName] {
return "", "", common.ErrModelNotFound
}
return modelName, "chat", nil
}
func TestResolveRelayModel(t *testing.T) {
// 后缀语法已移除:目录名即请求名,不再有「字面模型优先 / 剥后缀回退」两段查找
t.Run("catalog model resolves literally", func(t *testing.T) {
provider := &modelMappingStub{models: map[string]bool{"gpt-4o": true}}
lookup, err := resolveRelayModel(t.Context(), provider, "gpt-4o")
if err != nil {
t.Fatalf("resolveRelayModel() error = %v", err)
}
if lookup != "gpt-4o" {
t.Errorf("lookup model = %q, want %q", lookup, "gpt-4o")
}
})
// 名字带 -max 等字样的真实目录模型按字面命中,不做任何后缀解释
t.Run("literal model ending in effort-like word", func(t *testing.T) {
provider := &modelMappingStub{models: map[string]bool{"qwen3.8-max": true}}
lookup, err := resolveRelayModel(t.Context(), provider, "qwen3.8-max")
if err != nil {
t.Fatalf("resolveRelayModel() error = %v", err)
}
if lookup != "qwen3.8-max" {
t.Errorf("lookup model = %q, want %q", lookup, "qwen3.8-max")
}
if len(provider.calls) != 1 || provider.calls[0] != "qwen3.8-max" {
t.Errorf("lookup calls = %v, want exactly one literal lookup", provider.calls)
}
})
// 目录中不存在即报错,不再尝试剥后缀回退("o3-max" 不会命中 "o3")
t.Run("missing model returns error without suffix fallback", func(t *testing.T) {
provider := &modelMappingStub{models: map[string]bool{"o3": true}}
_, err := resolveRelayModel(t.Context(), provider, "o3-max")
if err == nil {
t.Fatal("want error for model absent from catalog")
}
if len(provider.calls) != 1 {
t.Errorf("lookup calls = %v, want single literal lookup", provider.calls)
}
})
}
// TestEstimateInputTokens 多模态请求体的预扣估算:base64 内联媒体必须按固定成本计入,
// 不能按字节折算——一张 2MB 的图按 len/4 会被估成约 70 万 token,预扣直接冻穿余额。
func TestEstimateInputTokens(t *testing.T) {
// 模拟 2MB 的 base64 图片载荷
payload := strings.Repeat("A", 2<<20)
cases := []struct {
name string
body string
want int
}{
{
"纯文本按字节折算",
strings.Repeat("x", 400),
100,
},
{
"空体",
"",
0,
},
{
// 文本骨架 40 字节 + 1 个媒体
"单图不按字节折算",
`{"url":"data:image/png;base64,` + payload + `"}`,
(len(`{"url":"data:image/png;base64,`+`"}`))/4 + inlineMediaTokenCost,
},
{
"双图按个数累加",
`{"a":"data:image/png;base64,` + payload + `","b":"data:image/png;base64,` + payload + `"}`,
(len(`{"a":"data:image/png;base64,`+`","b":"data:image/png;base64,`+`"}`))/4 + 2*inlineMediaTokenCost,
},
{
// 畸形 data URL(无 base64 标记)退化为纯文本路径
"畸形 data URL",
`{"url":"data:image/png,` + strings.Repeat("A", 400) + `"}`,
len(`{"url":"data:image/png,`+`"}`)/4 + 100,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := estimateInputTokens([]byte(c.body)); got != c.want {
t.Errorf("estimateInputTokens() = %d, want %d", got, c.want)
}
})
}
// 回归保护:2MB 图片的估算必须远低于按字节折算的量级
naive := len(`{"url":"data:image/png;base64,`+payload+`"}`) / 4
got := estimateInputTokens([]byte(`{"url":"data:image/png;base64,` + payload + `"}`))
if got >= naive/10 {
t.Errorf("2MB 图片估算 %d tokens,与按字节折算的 %d 处于同一量级,预扣仍会冻穿余额", got, naive)
}
}