Files
nyoungo 86492460b0 feat: 工具调用解析、Responses 状态缓存、幂等重放与模型别名
- 重写工具调用解析:XML、DSML 未加引号键、fence 内跳过、参数外溢等形状,并按请求携带的 tools 过滤工具名
- 新增 GET /v1/responses/{id} 与 previous_response_id 有界状态缓存(进程内、TTL、store=false 不写入)
- /v1/chat/completions 支持 Idempotency-Key 幂等回放(1 MiB 上限、24h TTL、并发去重)
- 模型别名与 default_model 降级;错误体统一输出 param/code
- 补 e2e 修复场景与 docs/compat-audit.md 兼容性审计
2026-10-09 00:50:01 +08:00

111 lines
3.0 KiB
Go

package config
import (
"strings"
"testing"
)
func aliasFixture(t *testing.T) *Config {
t.Helper()
cfg, err := Load(examplePath)
if err != nil {
t.Fatalf("load: %v", err)
}
cfg.ModelAliases = map[string]string{
"gpt-5": "deepseek-v4-flash-de",
"Claude-Sonnet-4-5": "deepseek-v4-pro-de",
}
cfg.DefaultModel = "deepseek-v4-flash-es"
return cfg
}
func TestResolveModelExactMatch(t *testing.T) {
cfg := aliasFixture(t)
m, resolved, ok := cfg.ResolveModel("deepseek-v4-pro-fr")
if !ok {
t.Fatal("exact id must resolve")
}
if resolved != "deepseek-v4-pro-fr" || m.Site != "fr" {
t.Fatalf("got (%q, site=%q)", resolved, m.Site)
}
}
func TestResolveModelAlias(t *testing.T) {
cfg := aliasFixture(t)
for _, want := range []string{"gpt-5", "GPT-5", "claude-sonnet-4-5"} {
m, resolved, ok := cfg.ResolveModel(want)
if !ok {
t.Fatalf("alias %q must resolve", want)
}
if resolved == want {
t.Fatalf("alias %q must be rewritten to a configured id, got %q", want, resolved)
}
if m == nil || !m.Enabled {
t.Fatalf("alias %q resolved to a bad model: %+v", want, m)
}
}
}
func TestResolveModelDefaultFallback(t *testing.T) {
cfg := aliasFixture(t)
m, resolved, ok := cfg.ResolveModel("totally-unknown-model")
if !ok {
t.Fatal("unknown model must fall back to default_model")
}
if resolved != "deepseek-v4-flash-es" {
t.Fatalf("want default model, got %q", resolved)
}
if m.Site != "es" {
t.Fatalf("want default model site es, got %q", m.Site)
}
}
func TestResolveModelNoFallbackWhenUnconfigured(t *testing.T) {
cfg := aliasFixture(t)
cfg.DefaultModel = ""
if _, _, ok := cfg.ResolveModel("totally-unknown-model"); ok {
t.Fatal("without default_model an unknown model must not resolve")
}
}
func TestResolveModelDisabledExactIdIsNotAliased(t *testing.T) {
cfg := aliasFixture(t)
cfg.Models["deepseek-v4-pro-fr"].Enabled = false
if _, _, ok := cfg.ResolveModel("deepseek-v4-pro-fr"); ok {
t.Fatal("a disabled model must stay 404 instead of falling back")
}
}
func TestValidateRejectsBadModelAliases(t *testing.T) {
cases := []struct {
name string
mut func(*Config)
match string
}{
{"alias target missing", func(c *Config) { c.ModelAliases["x"] = "no-such-model" }, "unknown model"},
{"alias target disabled", func(c *Config) {
c.Models["deepseek-v4-pro-fr"].Enabled = false
c.ModelAliases["x"] = "deepseek-v4-pro-fr"
}, "disabled"},
{"empty alias name", func(c *Config) { c.ModelAliases[""] = "deepseek-v4-pro-fr" }, "must not be empty"},
{"default missing", func(c *Config) { c.DefaultModel = "no-such-model" }, "default_model"},
{"default disabled", func(c *Config) {
c.Models["deepseek-v4-pro-fr"].Enabled = false
c.DefaultModel = "deepseek-v4-pro-fr"
}, "default_model"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := aliasFixture(t)
tc.mut(cfg)
err := cfg.Validate()
if err == nil {
t.Fatal("want validation error")
}
if !strings.Contains(err.Error(), tc.match) {
t.Fatalf("error %q does not mention %q", err, tc.match)
}
})
}
}