- 重写工具调用解析: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 兼容性审计
111 lines
3.0 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|