package llm import ( "reflect" "testing" ) func TestFilterAllowed(t *testing.T) { models := []string{"glm-4.7", "glm-4.7-air", "glm-4.6", "deepseek-v4-flash", "gpt-5-mini"} // 空白名单不筛选 if got := FilterAllowed(models, nil); !reflect.DeepEqual(got, models) { t.Errorf("FilterAllowed(nil) = %v, want unchanged", got) } // 前缀通配 if got := FilterAllowed(models, []string{"glm-*"}); !reflect.DeepEqual(got, []string{"glm-4.7", "glm-4.7-air", "glm-4.6"}) { t.Errorf("FilterAllowed(glm-*) = %v", got) } // 精确名大小写不敏感 + 通配混合 if got := FilterAllowed(models, []string{"GLM-4.7", "deepseek-*"}); !reflect.DeepEqual(got, []string{"glm-4.7", "deepseek-v4-flash"}) { t.Errorf("FilterAllowed(mixed) = %v", got) } // 全不匹配 if got := FilterAllowed(models, []string{"claude-*"}); len(got) != 0 { t.Errorf("FilterAllowed(claude-*) = %v, want empty", got) } } func TestNormalizeAPIFormat(t *testing.T) { cases := map[string]APIFormat{ "anthropic": FormatAnthropic, "Anthropic_Messages": FormatAnthropic, "openai": FormatChatCompletions, "chat": FormatChatCompletions, "chat_completions": FormatChatCompletions, "Responses": FormatResponses, "": "", "graphql": "", } for in, want := range cases { if got := NormalizeAPIFormat(in); got != want { t.Errorf("NormalizeAPIFormat(%q) = %q, want %q", in, got, want) } } } func TestNewProviderFormats(t *testing.T) { // 三种格式各自构造出对应 provider p1, err := NewProvider(ProviderSpec{BaseURL: "https://x.com", APIFormat: FormatAnthropic}) if err != nil || p1 == nil { t.Errorf("anthropic: %v", err) } p2, err := NewProvider(ProviderSpec{BaseURL: "https://x.com/v1", APIFormat: FormatChatCompletions}) if err != nil { t.Errorf("chat: %v", err) } if _, ok := p2.(*OpenAIProvider); !ok { t.Errorf("chat_completions should yield OpenAIProvider, got %T", p2) } p3, err := NewProvider(ProviderSpec{BaseURL: "https://x.com/v1", APIFormat: FormatResponses}) if err != nil { t.Errorf("responses: %v", err) } if _, ok := p3.(*ResponsesProvider); !ok { t.Errorf("responses should yield ResponsesProvider, got %T", p3) } // 未知格式报错 if _, err := NewProvider(ProviderSpec{BaseURL: "https://x.com", APIFormat: "graphql"}); err == nil { t.Error("unknown format should error") } // BaseURL 含 anthropic 时自动推断 p4, _ := NewProvider(ProviderSpec{BaseURL: "https://api.anthropic.com"}) if _, ok := p4.(*AnthropicProvider); !ok { t.Errorf("anthropic URL should infer AnthropicProvider, got %T", p4) } } // TestMissingRegisteredModels:下架判定——大小写不敏感、通配条目不参与 // 判定(既不标下架也不算存活)、空目录不判定、原文保序去重。 func TestMissingRegisteredModels(t *testing.T) { fetched := []string{"glm-4.7", "GLM-4.6", "cogview-4", "glm-4.5-air"} cases := []struct { name string registered []string fetched []string want []string }{ { name: "精确匹配大小写不敏感", registered: []string{"glm-4.7", "glm-4.6", "dead-model"}, fetched: fetched, want: []string{"dead-model"}, }, { name: "通配条目不参与判定", registered: []string{"glm-*", "kling-*"}, fetched: fetched, want: nil, // 模式无法对具体目录下结论:跳过,不标下架 }, { name: "空目录不判定(拉取失败不误标)", registered: []string{"glm-4.7"}, fetched: nil, want: nil, }, { name: "保序去重、跳过空条目", registered: []string{"b-dead", "", "a-dead", "B-DEAD"}, fetched: fetched, want: []string{"b-dead", "a-dead"}, }, { name: "全部存活返回 nil", registered: []string{"GLM-4.7", "cogview-4"}, fetched: fetched, want: nil, }, } for _, c := range cases { got := MissingRegisteredModels(c.registered, c.fetched) if len(got) != len(c.want) { t.Errorf("%s: got %v, want %v", c.name, got, c.want) continue } for i := range got { if got[i] != c.want[i] { t.Errorf("%s: got[%d]=%q, want %q", c.name, i, got[i], c.want[i]) } } } } // TestFilterAllowed 与 MissingRegisteredModels 口径一致:同一个匹配函数。 func TestFilterAllowedWildcard(t *testing.T) { got := FilterAllowed([]string{"glm-4.7", "cogview-4"}, []string{"glm-*"}) if len(got) != 1 || got[0] != "glm-4.7" { t.Errorf("FilterAllowed = %v, want [glm-4.7]", got) } }