package runtimeconfig import ( "encoding/json" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" ) // 本文件钉住 provider 级推理开关形状的配置面(裁决 03.4.2 / 08.4.1):每个 // provider 组各声明自己的形状,只有本版本实现且有依据的形状才被接受,未声明的 // 组在 models.json 里不留下空对象(保守默认 = 不发任何推理字段)。 // TestValidateReasoningKind 覆盖合法/非法形状:未知形状必须让保存失败(否则 // 「配了却不生效」正是裁决要消灭的假适配),anthropic 组不允许声明 OpenAI 侧形状。 func TestValidateReasoningKind(t *testing.T) { t.Parallel() cases := []struct { name string provider string kind string wantErr string }{ {name: "unset", provider: ProviderOpenAI}, {name: "auto", provider: ProviderOpenAI, kind: ReasoningKindAuto}, {name: "deepseek", provider: ProviderOpenAI, kind: ReasoningKindDeepSeek}, {name: "openai", provider: ProviderOpenAI, kind: ReasoningKindOpenAI}, {name: "vllm", provider: ProviderOpenAI, kind: ReasoningKindVLLM}, {name: "dashscope", provider: ProviderOpenAI, kind: ReasoningKindDashScope}, {name: "padded", provider: ProviderOpenAI, kind: " vllm "}, {name: "auto_on_anthropic", provider: ProviderAnthropic, kind: ReasoningKindAuto}, {name: "unknown", provider: ProviderOpenAI, kind: "gemini", wantErr: "must be one of"}, {name: "openai_shape_on_anthropic", provider: ProviderAnthropic, kind: ReasoningKindVLLM, wantErr: "openai-compatible"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() g := Group{ ID: "g", Provider: tc.provider, BaseURL: "https://x/v1", Model: "m", Reasoning: ReasoningOptions{Kind: tc.kind}, } err := g.Validate() if tc.wantErr == "" { if err != nil { t.Fatalf("Validate() = %v, want nil for kind %q", err, tc.kind) } return } if err == nil || !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("Validate() = %v, want an error containing %q", err, tc.wantErr) } }) } } // TestReasoningKindJSONShape asserts an undeclared shape is absent from models.json // (no empty object churn on every group) and a declared one round-trips by name. func TestReasoningKindJSONShape(t *testing.T) { t.Parallel() plain := Group{ID: "g", Provider: ProviderOpenAI, BaseURL: "https://x/v1", Model: "m"} raw, err := json.Marshal(plain) if err != nil { t.Fatalf("Marshal: %v", err) } if strings.Contains(string(raw), "reasoning") { t.Fatalf("marshal = %s, want no reasoning key for an undeclared shape", raw) } plain.Reasoning.Kind = ReasoningKindDashScope raw, err = json.Marshal(plain) if err != nil { t.Fatalf("Marshal: %v", err) } if !strings.Contains(string(raw), `"reasoning":{"kind":"dashscope"}`) { t.Fatalf("marshal = %s, want the declared shape serialized", raw) } path := t.TempDir() + "/models.json" if err := SaveModels(path, []Group{plain}); err != nil { t.Fatalf("SaveModels: %v", err) } groups, err := LoadModels(path, config.LLMConfig{}) if err != nil { t.Fatalf("LoadModels: %v", err) } if got := groups[0].Reasoning.Kind; got != ReasoningKindDashScope { t.Fatalf("round-tripped kind = %q, want %q", got, ReasoningKindDashScope) } } // TestToLLMConfigCarriesReasoningKind 断言形状随组配置抵达适配器那一层(否则 // models.json 的声明会静默失效)。 func TestToLLMConfigCarriesReasoningKind(t *testing.T) { t.Parallel() for _, provider := range []string{ProviderOpenAI, ProviderAnthropic} { g := Group{ ID: "g", Provider: provider, BaseURL: "https://x", APIKey: "k", Model: "m", Reasoning: ReasoningOptions{Kind: " " + ReasoningKindDeepSeek + " "}, } if got := g.ToLLMConfig().ReasoningKind; got != ReasoningKindDeepSeek { t.Errorf("provider %s: ReasoningKind = %q, want the trimmed declared shape", provider, got) } } } // TestGroupFromEnvCarriesReasoningKind pins the env fallback twin: an env-only // deployment can declare its endpoint's shape, and its default stays blank so no // reasoning field is ever sent without an explicit declaration. func TestGroupFromEnvCarriesReasoningKind(t *testing.T) { t.Parallel() if got := GroupFromEnv(config.LLMConfig{})[0].Reasoning.Kind; got != "" { t.Fatalf("env default kind = %q, want empty (conservative)", got) } env := config.LLMConfig{BaseURL: "https://x/v1", APIKey: "k", Model: "m", ReasoningKind: " vllm "} groups := GroupFromEnv(env) if len(groups) != 1 { t.Fatalf("groups = %d, want 1", len(groups)) } if got := groups[0].Reasoning.Kind; got != ReasoningKindVLLM { t.Fatalf("env kind = %q, want %q", got, ReasoningKindVLLM) } if err := groups[0].Validate(); err != nil { t.Fatalf("a fully configured env group must validate: %v", err) } }