package majordomo import ( "context" "errors" "io" "net/http" "strings" "testing" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" ) // kimiResponse is a minimal valid Chat Completions body so Generate returns a // non-empty response (an empty one would trigger failover, not a clean pass). const kimiResponse = `{"id":"c1","object":"chat.completion","choices":[` + `{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}]}` // captureRT records the last request and returns a canned response without // touching the network, so these tests stay hermetic while still exercising // the real openai client the kimi built-in reuses (base URL + auth header). type captureRT struct { req *http.Request body string } func (c *captureRT) RoundTrip(r *http.Request) (*http.Response, error) { c.req = r return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(c.body)), Header: make(http.Header), Request: r, }, nil } // TestKimiBuiltin: the built-in "kimi" provider resolves in Parse, targets // Moonshot's default endpoint, and authenticates with KIMI_API_KEY. func TestKimiBuiltin(t *testing.T) { rt := &captureRT{body: kimiResponse} r := newTestRegistry(t, WithEnvLookup(func(k string) string { if k == "KIMI_API_KEY" { return "kimi-secret" } return "" }), WithHTTPClient(&http.Client{Transport: rt}), ) if p, ok := r.Provider(ProviderKimi); !ok { t.Fatal("built-in kimi provider not registered") } else if p.Name() != ProviderKimi { t.Errorf("name = %q, want %q", p.Name(), ProviderKimi) } m, err := r.Parse("kimi/kimi-k2-0711-preview") if err != nil { t.Fatalf("Parse: %v", err) } if got := targetsOf(t, m); len(got) != 1 || got[0] != "kimi/kimi-k2-0711-preview" { t.Fatalf("targets = %v", got) } if _, err := m.Generate(context.Background(), llm.Request{Messages: []llm.Message{llm.UserText("hi")}}); err != nil { t.Fatalf("Generate: %v", err) } if rt.req == nil { t.Fatal("no request captured") } if want := "https://api.moonshot.ai/v1/chat/completions"; rt.req.URL.String() != want { t.Errorf("URL = %q, want %q", rt.req.URL.String(), want) } if want := "Bearer kimi-secret"; rt.req.Header.Get("Authorization") != want { t.Errorf("Authorization = %q, want %q", rt.req.Header.Get("Authorization"), want) } } // TestKimiBuiltinMissingKey: with no KIMI_API_KEY the built-in fails fast with a // synthetic 401 whose hint names KIMI_API_KEY — never OPENAI_API_KEY (proving // the credential does not fall through to the openai client's default), and // without hitting the network. func TestKimiBuiltinMissingKey(t *testing.T) { rt := &captureRT{body: kimiResponse} r := newTestRegistry(t, WithHTTPClient(&http.Client{Transport: rt})) m, err := r.Parse("kimi/kimi-k2-0711-preview") if err != nil { t.Fatalf("Parse: %v", err) } _, err = m.Generate(context.Background(), llm.Request{Messages: []llm.Message{llm.UserText("hi")}}) apiErr, ok := errors.AsType[*llm.APIError](err) if !ok { t.Fatalf("err = %v (%T), want *llm.APIError", err, err) } if apiErr.Status != http.StatusUnauthorized || apiErr.Code != "missing_api_key" { t.Errorf("Status/Code = %d/%q, want 401/missing_api_key", apiErr.Status, apiErr.Code) } if !strings.Contains(apiErr.Message, "KIMI_API_KEY") { t.Errorf("message = %q, want it to name KIMI_API_KEY", apiErr.Message) } if strings.Contains(apiErr.Message, "OPENAI_API_KEY") { t.Errorf("message = %q, must not name OPENAI_API_KEY", apiErr.Message) } if rt.req != nil { t.Error("network was hit despite missing key") } } // TestKimiScheme: a kimi:// LLM_* DSN defines a named provider on any Moonshot // host (here the China endpoint) that is first-class in Parse and carries the // DSN token as its bearer credential. func TestKimiScheme(t *testing.T) { rt := &captureRT{body: kimiResponse} r := newTestRegistry(t, WithHTTPClient(&http.Client{Transport: rt})) if err := r.LoadEnv(map[string]string{ "LLM_KCN": "kimi://tok@api.moonshot.cn/v1", }); err != nil { t.Fatalf("LoadEnv: %v", err) } m, err := r.Parse("kcn/moonshot-v1-8k") if err != nil { t.Fatalf("Parse: %v", err) } if _, err := m.Generate(context.Background(), llm.Request{Messages: []llm.Message{llm.UserText("hi")}}); err != nil { t.Fatalf("Generate: %v", err) } if rt.req == nil { t.Fatal("no request captured") } if want := "https://api.moonshot.cn/v1/chat/completions"; rt.req.URL.String() != want { t.Errorf("URL = %q, want %q", rt.req.URL.String(), want) } if want := "Bearer tok"; rt.req.Header.Get("Authorization") != want { t.Errorf("Authorization = %q, want %q", rt.req.Header.Get("Authorization"), want) } } // TestKimiSchemeMissingToken: a kimi:// DSN with no token is fixed by adding one // to the DSN, not by setting KIMI_API_KEY — so the missing-key hint names the // defining LLM_ env var, never KIMI_API_KEY (which does nothing for a // DSN-defined provider). func TestKimiSchemeMissingToken(t *testing.T) { rt := &captureRT{body: kimiResponse} r := newTestRegistry(t, WithHTTPClient(&http.Client{Transport: rt})) if err := r.LoadEnv(map[string]string{ "LLM_KCN": "kimi://api.moonshot.cn/v1", // no token }); err != nil { t.Fatalf("LoadEnv: %v", err) } m, err := r.Parse("kcn/moonshot-v1-8k") if err != nil { t.Fatalf("Parse: %v", err) } _, err = m.Generate(context.Background(), llm.Request{Messages: []llm.Message{llm.UserText("hi")}}) apiErr, ok := errors.AsType[*llm.APIError](err) if !ok { t.Fatalf("err = %v (%T), want *llm.APIError", err, err) } if !strings.Contains(apiErr.Message, "LLM_KCN") { t.Errorf("message = %q, want it to name LLM_KCN", apiErr.Message) } if strings.Contains(apiErr.Message, "KIMI_API_KEY") { t.Errorf("message = %q, must not name KIMI_API_KEY for a DSN provider", apiErr.Message) } if rt.req != nil { t.Error("network was hit despite missing token") } }