From 570fe263c7cc1d9e7b7433e8299cb97ffb151e6b Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:33:47 +0000 Subject: [PATCH] Add MiniMax runner to the semantic-comparison factory Introduce a direct MiniMax HTTP runner that implements the AgentRunner interface, register it under the "minimax" identifier in the runner factory, and configure its text models and regional endpoints through environment variables. Update the ENGRAM_AGENT_CLI guidance accordingly. --- cmd/engram/llm.go | 2 +- cmd/engram/main.go | 2 +- internal/llm/factory.go | 10 +- internal/llm/factory_test.go | 11 ++ internal/llm/minimax.go | 245 ++++++++++++++++++++++++++++++++++ internal/llm/minimax_test.go | 248 +++++++++++++++++++++++++++++++++++ 6 files changed, 513 insertions(+), 5 deletions(-) create mode 100644 internal/llm/minimax.go create mode 100644 internal/llm/minimax_test.go diff --git a/cmd/engram/llm.go b/cmd/engram/llm.go index 473b0c39..99895094 100644 --- a/cmd/engram/llm.go +++ b/cmd/engram/llm.go @@ -69,7 +69,7 @@ func llmBuildPrompt(a, b store.ObservationSnippet) string { func resolveAgentRunner() (store.SemanticRunner, error) { name := os.Getenv("ENGRAM_AGENT_CLI") if name == "" { - return nil, errors.New("ENGRAM_AGENT_CLI is not set; required for --semantic scan (set to 'claude' or 'opencode')") + return nil, errors.New("ENGRAM_AGENT_CLI is not set; required for --semantic scan (set to 'claude', 'opencode', or 'minimax')") } return agentRunnerFactory(name) } diff --git a/cmd/engram/main.go b/cmd/engram/main.go index 730d783f..11795076 100644 --- a/cmd/engram/main.go +++ b/cmd/engram/main.go @@ -2721,7 +2721,7 @@ Environment: ENGRAM_TIMEZONE Timezone for timestamp display in TUI and cloud dashboard. Accepts any IANA zone name (e.g. America/New_York, Europe/Berlin). Falls back to system local time when unset or invalid. - ENGRAM_AGENT_CLI LLM runner for conflicts scan --semantic (claude or opencode) + ENGRAM_AGENT_CLI LLM runner for conflicts scan --semantic (claude, opencode, or minimax) ENGRAM_CLOUD_AUTOSYNC Set to 1 to enable background autosync; also requires ENGRAM_CLOUD_TOKEN and ENGRAM_CLOUD_SERVER diff --git a/internal/llm/factory.go b/internal/llm/factory.go index 5ed7d043..00b8caf9 100644 --- a/internal/llm/factory.go +++ b/internal/llm/factory.go @@ -8,7 +8,7 @@ import ( // ─── Sentinel errors ────────────────────────────────────────────────────────── // ErrInvalidRunnerName is returned by NewRunner when the name argument does not -// match a known runner identifier ("claude" | "opencode"). +// match a known runner identifier ("claude" | "opencode" | "minimax"). var ErrInvalidRunnerName = errors.New("invalid runner name") // ─── Factory ────────────────────────────────────────────────────────────────── @@ -17,6 +17,7 @@ var ErrInvalidRunnerName = errors.New("invalid runner name") // Supported values: // - "claude" → *ClaudeRunner (shells out to the claude CLI) // - "opencode" → *OpenCodeRunner (shells out to the opencode CLI) +// - "minimax" → *MiniMaxRunner (calls the MiniMax hosted API over HTTP) // // For any other value, including the empty string, a descriptive error is // returned that names the ENGRAM_AGENT_CLI environment variable and the @@ -33,15 +34,18 @@ func NewRunner(name string) (AgentRunner, error) { case "opencode": return NewOpenCodeRunner(), nil + case "minimax": + return NewMiniMaxRunner(), nil + case "": return nil, fmt.Errorf( - "%w: ENGRAM_AGENT_CLI is not set; supported values are: claude, opencode", + "%w: ENGRAM_AGENT_CLI is not set; supported values are: claude, opencode, minimax", ErrInvalidRunnerName, ) default: return nil, fmt.Errorf( - "%w: %q is not a recognized runner; set ENGRAM_AGENT_CLI to one of: claude, opencode", + "%w: %q is not a recognized runner; set ENGRAM_AGENT_CLI to one of: claude, opencode, minimax", ErrInvalidRunnerName, name, ) diff --git a/internal/llm/factory_test.go b/internal/llm/factory_test.go index 6f134473..422aef84 100644 --- a/internal/llm/factory_test.go +++ b/internal/llm/factory_test.go @@ -29,6 +29,17 @@ func TestNewRunner_OpenCode(t *testing.T) { } } +// TestNewRunner_MiniMax verifies that "minimax" returns a *MiniMaxRunner without error. +func TestNewRunner_MiniMax(t *testing.T) { + runner, err := llm.NewRunner("minimax") + if err != nil { + t.Fatalf("NewRunner(\"minimax\"): unexpected error: %v", err) + } + if _, ok := runner.(*llm.MiniMaxRunner); !ok { + t.Errorf("NewRunner(\"minimax\") returned %T; want *llm.MiniMaxRunner", runner) + } +} + // TestNewRunner_Empty verifies that an empty string returns a descriptive error // naming the ENGRAM_AGENT_CLI env var. func TestNewRunner_Empty(t *testing.T) { diff --git a/internal/llm/minimax.go b/internal/llm/minimax.go new file mode 100644 index 00000000..bf5d2d01 --- /dev/null +++ b/internal/llm/minimax.go @@ -0,0 +1,245 @@ +package llm + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" +) + +// ─── MiniMax provider configuration ──────────────────────────────────────────── + +// Text model identifiers accepted by the MiniMax runner. +const ( + // MiniMaxDefaultModel is used when ENGRAM_MINIMAX_MODEL is unset. + MiniMaxDefaultModel = "MiniMax-M3" + + // MiniMaxModelM3 is the flagship text model (1,000,000-token context). + MiniMaxModelM3 = "MiniMax-M3" + + // MiniMaxModelM27 is the compact text model (204,800-token context). + MiniMaxModelM27 = "MiniMax-M2.7" +) + +// MiniMaxModelIDs lists the text models this runner is configured for. +var MiniMaxModelIDs = []string{MiniMaxModelM3, MiniMaxModelM27} + +// Regional API configuration. ENGRAM_MINIMAX_REGION selects a region and +// ENGRAM_MINIMAX_BASE_URL overrides the resolved base URL entirely. +const ( + // MiniMaxRegionGlobal is the default region key. + MiniMaxRegionGlobal = "global_en" + + // MiniMaxRegionChina is the alternate region key. + MiniMaxRegionChina = "cn_zh" + + miniMaxBaseURLGlobal = "https://api.minimax.io/v1" + miniMaxBaseURLChina = "https://api.minimaxi.com/v1" + + // miniMaxChatPath is appended to the resolved base URL to reach the + // chat completions endpoint. + miniMaxChatPath = "/chat/completions" +) + +// miniMaxHTTPClient is the shared client used for outbound requests. It carries +// a conservative backstop timeout; per-call deadlines come from the context. +var miniMaxHTTPClient = &http.Client{Timeout: 60 * time.Second} + +// ─── MiniMaxRunner ────────────────────────────────────────────────────────────── + +// MiniMaxRunner implements AgentRunner by calling the MiniMax hosted chat +// completions API directly over HTTP. It sends the canonical comparison prompt +// as a single user message and parses the returned Verdict JSON. +type MiniMaxRunner struct { + // baseURL is the API base (without the chat completions path). + baseURL string + + // apiKey authenticates the request via a bearer token. + apiKey string + + // model is the text model identifier sent with each request. + model string + + // doRequest is the HTTP round-trip seam. Defaults to miniMaxHTTPClient.Do. + // Tests inject a fake implementation to avoid real network calls. + doRequest func(*http.Request) (*http.Response, error) +} + +// NewMiniMaxRunner constructs a MiniMaxRunner from the environment: +// +// MINIMAX_API_KEY bearer token (required at Compare time) +// ENGRAM_MINIMAX_MODEL text model id; defaults to MiniMaxDefaultModel +// ENGRAM_MINIMAX_REGION "global_en" (default) or "cn_zh" +// ENGRAM_MINIMAX_BASE_URL optional explicit base URL override +// +// Missing credentials are reported as an error from Compare rather than at +// construction, mirroring the other runners' lazy-failure behavior. +func NewMiniMaxRunner() *MiniMaxRunner { + return &MiniMaxRunner{ + baseURL: miniMaxResolveBaseURL(), + apiKey: strings.TrimSpace(os.Getenv("MINIMAX_API_KEY")), + model: miniMaxResolveModel(), + doRequest: miniMaxHTTPClient.Do, + } +} + +// miniMaxResolveBaseURL selects the base URL from the environment. +func miniMaxResolveBaseURL() string { + if v := strings.TrimSpace(os.Getenv("ENGRAM_MINIMAX_BASE_URL")); v != "" { + return strings.TrimRight(v, "/") + } + switch strings.TrimSpace(os.Getenv("ENGRAM_MINIMAX_REGION")) { + case MiniMaxRegionChina: + return miniMaxBaseURLChina + default: + return miniMaxBaseURLGlobal + } +} + +// miniMaxResolveModel selects the model id from the environment. +func miniMaxResolveModel() string { + if v := strings.TrimSpace(os.Getenv("ENGRAM_MINIMAX_MODEL")); v != "" { + return v + } + return MiniMaxDefaultModel +} + +// Compare sends prompt to the MiniMax chat completions endpoint and returns a +// structured Verdict. +// +// The request body carries the model and a single user message; the response's +// first choice message content is parsed as the Verdict JSON (markdown code +// fences are stripped before parsing). +func (r *MiniMaxRunner) Compare(ctx context.Context, prompt string) (Verdict, error) { + if r.apiKey == "" { + return Verdict{}, fmt.Errorf("%w: MINIMAX_API_KEY is not set", ErrCLIAuthMissing) + } + + payload, err := json.Marshal(miniMaxChatRequest{ + Model: r.model, + Stream: false, + Messages: []miniMaxMessage{ + {Role: "user", Content: prompt}, + }, + }) + if err != nil { + return Verdict{}, fmt.Errorf("minimax: encode request: %v", err) + } + + start := time.Now() + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, r.baseURL+miniMaxChatPath, bytes.NewReader(payload)) + if err != nil { + return Verdict{}, fmt.Errorf("minimax: build request: %v", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+r.apiKey) + + do := r.doRequest + if do == nil { + do = miniMaxHTTPClient.Do + } + resp, err := do(httpReq) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return Verdict{}, fmt.Errorf("%w: minimax request", ErrTimeout) + } + return Verdict{}, fmt.Errorf("minimax: request failed: %w", err) + } + defer resp.Body.Close() + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return Verdict{}, fmt.Errorf("minimax: read response: %v", err) + } + if resp.StatusCode != http.StatusOK { + return Verdict{}, fmt.Errorf("minimax: API returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw))) + } + + return parseMiniMaxResponse(raw, time.Since(start).Milliseconds()) +} + +// ─── Compile-time interface satisfaction ─────────────────────────────────────── + +var _ AgentRunner = (*MiniMaxRunner)(nil) + +// ─── Request / response shapes ────────────────────────────────────────────────── + +// miniMaxChatRequest is the chat completions request body. +type miniMaxChatRequest struct { + Model string `json:"model"` + Stream bool `json:"stream"` + Messages []miniMaxMessage `json:"messages"` +} + +// miniMaxMessage is a single chat message. +type miniMaxMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// miniMaxChatResponse is the subset of the chat completions response we read. +type miniMaxChatResponse struct { + Model string `json:"model"` + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + BaseResp struct { + StatusCode int `json:"status_code"` + StatusMsg string `json:"status_msg"` + } `json:"base_resp"` +} + +// parseMiniMaxResponse decodes the chat completions response and returns a Verdict. +func parseMiniMaxResponse(raw []byte, durationMS int64) (Verdict, error) { + var env miniMaxChatResponse + if err := json.Unmarshal(raw, &env); err != nil { + return Verdict{}, fmt.Errorf("%w: response envelope: %v", ErrInvalidJSON, err) + } + + // A non-zero status_code signals an application-level failure even when the + // HTTP status is 200. + if env.BaseResp.StatusCode != 0 { + return Verdict{}, fmt.Errorf("minimax: API error %d: %s", env.BaseResp.StatusCode, env.BaseResp.StatusMsg) + } + + if len(env.Choices) == 0 { + return Verdict{}, fmt.Errorf("%w: response contained no choices", ErrInvalidJSON) + } + + // Strip markdown fences from the message content before JSON parsing. + content := strings.TrimSpace(env.Choices[0].Message.Content) + if m := fenceRE.FindStringSubmatch(content); len(m) == 2 { + content = strings.TrimSpace(m[1]) + } + + var iv innerVerdict + if err := json.Unmarshal([]byte(content), &iv); err != nil { + return Verdict{}, fmt.Errorf("%w: inner verdict: %v", ErrInvalidJSON, err) + } + + if !validRelations[iv.Relation] { + return Verdict{}, fmt.Errorf("%w: %q", ErrUnknownRelation, iv.Relation) + } + + // Prefer the model reported by the API, falling back to the inner field. + model := env.Model + if model == "" { + model = iv.Model + } + + return Verdict{ + Relation: iv.Relation, + Confidence: iv.Confidence, + Reasoning: iv.Reasoning, + Model: model, + DurationMS: durationMS, + }, nil +} diff --git a/internal/llm/minimax_test.go b/internal/llm/minimax_test.go new file mode 100644 index 00000000..afee27e0 --- /dev/null +++ b/internal/llm/minimax_test.go @@ -0,0 +1,248 @@ +package llm + +// This test file lives in package llm (not llm_test) so it can inject the +// unexported doRequest seam and exercise the request/response internals +// without performing real network calls. + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "testing" +) + +// ─── helpers ────────────────────────────────────────────────────────────────── + +// fakeHTTPResponse builds an *http.Response with the given status and body. +func fakeHTTPResponse(status int, body string) *http.Response { + return &http.Response{ + StatusCode: status, + Body: io.NopCloser(strings.NewReader(body)), + Header: make(http.Header), + } +} + +// fakeDo returns a doRequest func that always returns the given response/error. +func fakeDo(resp *http.Response, err error) func(*http.Request) (*http.Response, error) { + return func(*http.Request) (*http.Response, error) { + return resp, err + } +} + +// chatEnvelope renders a chat completions response body wrapping innerContent. +func chatEnvelope(model, innerContent string) string { + return fmt.Sprintf( + `{"model":%q,"choices":[{"message":{"role":"assistant","content":%q}}],"base_resp":{"status_code":0,"status_msg":"success"}}`, + model, innerContent, + ) +} + +func newTestRunner(do func(*http.Request) (*http.Response, error)) *MiniMaxRunner { + return &MiniMaxRunner{ + baseURL: miniMaxBaseURLGlobal, + apiKey: "test-key", + model: MiniMaxDefaultModel, + doRequest: do, + } +} + +// ─── MiniMaxRunner tests ──────────────────────────────────────────────────────── + +// TestMiniMaxRunner_CompileTimeCheck verifies MiniMaxRunner satisfies AgentRunner. +var _ AgentRunner = (*MiniMaxRunner)(nil) + +func TestMiniMaxRunner_GoldenResponse(t *testing.T) { + inner := `{"Relation":"supersedes","Confidence":0.91,"Reasoning":"A replaces B"}` + body := chatEnvelope("MiniMax-M3", inner) + + r := newTestRunner(fakeDo(fakeHTTPResponse(http.StatusOK, body), nil)) + v, err := r.Compare(context.Background(), "compare these two") + if err != nil { + t.Fatalf("Compare: unexpected error: %v", err) + } + if v.Relation != "supersedes" { + t.Errorf("Relation = %q; want %q", v.Relation, "supersedes") + } + if v.Confidence != 0.91 { + t.Errorf("Confidence = %v; want 0.91", v.Confidence) + } + if v.Reasoning != "A replaces B" { + t.Errorf("Reasoning = %q; want %q", v.Reasoning, "A replaces B") + } + if v.Model != "MiniMax-M3" { + t.Errorf("Model = %q; want %q", v.Model, "MiniMax-M3") + } +} + +func TestMiniMaxRunner_FenceStripping(t *testing.T) { + inner := "```json\n{\"Relation\":\"compatible\",\"Confidence\":0.8,\"Reasoning\":\"They agree\"}\n```" + body := chatEnvelope("MiniMax-M2.7", inner) + + r := newTestRunner(fakeDo(fakeHTTPResponse(http.StatusOK, body), nil)) + v, err := r.Compare(context.Background(), "compare") + if err != nil { + t.Fatalf("Compare with fence: unexpected error: %v", err) + } + if v.Relation != "compatible" { + t.Errorf("Relation = %q; want %q", v.Relation, "compatible") + } + if v.Model != "MiniMax-M2.7" { + t.Errorf("Model = %q; want %q", v.Model, "MiniMax-M2.7") + } +} + +func TestMiniMaxRunner_RequestShape(t *testing.T) { + var captured *http.Request + var capturedBody []byte + do := func(req *http.Request) (*http.Response, error) { + captured = req + capturedBody, _ = io.ReadAll(req.Body) + inner := `{"Relation":"related","Confidence":0.6,"Reasoning":"shared topic"}` + return fakeHTTPResponse(http.StatusOK, chatEnvelope("MiniMax-M3", inner)), nil + } + + r := newTestRunner(do) + if _, err := r.Compare(context.Background(), "the prompt body"); err != nil { + t.Fatalf("Compare: unexpected error: %v", err) + } + + if captured.Method != http.MethodPost { + t.Errorf("method = %q; want POST", captured.Method) + } + if !strings.HasSuffix(captured.URL.String(), "/chat/completions") { + t.Errorf("URL = %q; want suffix /chat/completions", captured.URL.String()) + } + if got := captured.Header.Get("Authorization"); got != "Bearer test-key" { + t.Errorf("Authorization = %q; want %q", got, "Bearer test-key") + } + if got := captured.Header.Get("Content-Type"); got != "application/json" { + t.Errorf("Content-Type = %q; want application/json", got) + } + + var reqBody miniMaxChatRequest + if err := json.Unmarshal(capturedBody, &reqBody); err != nil { + t.Fatalf("request body not valid JSON: %v", err) + } + if reqBody.Model != MiniMaxDefaultModel { + t.Errorf("request model = %q; want %q", reqBody.Model, MiniMaxDefaultModel) + } + if len(reqBody.Messages) != 1 || reqBody.Messages[0].Content != "the prompt body" { + t.Errorf("request messages = %+v; want single user message with the prompt", reqBody.Messages) + } +} + +func TestMiniMaxRunner_MissingAPIKey(t *testing.T) { + r := &MiniMaxRunner{baseURL: miniMaxBaseURLGlobal, model: MiniMaxDefaultModel, doRequest: fakeDo(nil, nil)} + _, err := r.Compare(context.Background(), "compare") + if !errors.Is(err, ErrCLIAuthMissing) { + t.Errorf("expected ErrCLIAuthMissing; got %v", err) + } +} + +func TestMiniMaxRunner_BaseRespError(t *testing.T) { + body := `{"model":"MiniMax-M3","choices":[],"base_resp":{"status_code":1004,"status_msg":"auth failed"}}` + r := newTestRunner(fakeDo(fakeHTTPResponse(http.StatusOK, body), nil)) + _, err := r.Compare(context.Background(), "compare") + if err == nil || !strings.Contains(err.Error(), "1004") { + t.Errorf("expected API error mentioning status_code 1004; got %v", err) + } +} + +func TestMiniMaxRunner_NoChoices(t *testing.T) { + body := `{"model":"MiniMax-M3","choices":[],"base_resp":{"status_code":0}}` + r := newTestRunner(fakeDo(fakeHTTPResponse(http.StatusOK, body), nil)) + _, err := r.Compare(context.Background(), "compare") + if !errors.Is(err, ErrInvalidJSON) { + t.Errorf("expected ErrInvalidJSON; got %v", err) + } +} + +func TestMiniMaxRunner_InvalidInnerJSON(t *testing.T) { + body := chatEnvelope("MiniMax-M3", "not valid json") + r := newTestRunner(fakeDo(fakeHTTPResponse(http.StatusOK, body), nil)) + _, err := r.Compare(context.Background(), "compare") + if !errors.Is(err, ErrInvalidJSON) { + t.Errorf("expected ErrInvalidJSON; got %v", err) + } +} + +func TestMiniMaxRunner_UnknownRelation(t *testing.T) { + inner := `{"Relation":"maybe_conflict","Confidence":0.5,"Reasoning":"dunno"}` + body := chatEnvelope("MiniMax-M3", inner) + r := newTestRunner(fakeDo(fakeHTTPResponse(http.StatusOK, body), nil)) + _, err := r.Compare(context.Background(), "compare") + if !errors.Is(err, ErrUnknownRelation) { + t.Errorf("expected ErrUnknownRelation; got %v", err) + } +} + +func TestMiniMaxRunner_Non200(t *testing.T) { + r := newTestRunner(fakeDo(fakeHTTPResponse(http.StatusInternalServerError, "boom"), nil)) + _, err := r.Compare(context.Background(), "compare") + if err == nil || !strings.Contains(err.Error(), "500") { + t.Errorf("expected error mentioning status 500; got %v", err) + } +} + +func TestMiniMaxRunner_TransportError(t *testing.T) { + transportErr := errors.New("connection refused") + r := newTestRunner(fakeDo(nil, transportErr)) + _, err := r.Compare(context.Background(), "compare") + if !errors.Is(err, transportErr) { + t.Errorf("expected wrapped transport error; got %v", err) + } +} + +func TestMiniMaxRunner_Timeout(t *testing.T) { + timeoutErr := fmt.Errorf("Post: %w", context.DeadlineExceeded) + r := newTestRunner(fakeDo(nil, timeoutErr)) + _, err := r.Compare(context.Background(), "compare") + if !errors.Is(err, ErrTimeout) { + t.Errorf("expected ErrTimeout; got %v", err) + } +} + +// ─── configuration resolution ────────────────────────────────────────────────── + +func TestMiniMaxResolveBaseURL(t *testing.T) { + t.Run("default is global", func(t *testing.T) { + t.Setenv("ENGRAM_MINIMAX_BASE_URL", "") + t.Setenv("ENGRAM_MINIMAX_REGION", "") + if got := miniMaxResolveBaseURL(); got != miniMaxBaseURLGlobal { + t.Errorf("baseURL = %q; want %q", got, miniMaxBaseURLGlobal) + } + }) + t.Run("china region", func(t *testing.T) { + t.Setenv("ENGRAM_MINIMAX_BASE_URL", "") + t.Setenv("ENGRAM_MINIMAX_REGION", MiniMaxRegionChina) + if got := miniMaxResolveBaseURL(); got != miniMaxBaseURLChina { + t.Errorf("baseURL = %q; want %q", got, miniMaxBaseURLChina) + } + }) + t.Run("explicit override wins", func(t *testing.T) { + t.Setenv("ENGRAM_MINIMAX_REGION", MiniMaxRegionChina) + t.Setenv("ENGRAM_MINIMAX_BASE_URL", "https://example.test/api/") + if got := miniMaxResolveBaseURL(); got != "https://example.test/api" { + t.Errorf("baseURL = %q; want trimmed override", got) + } + }) +} + +func TestMiniMaxResolveModel(t *testing.T) { + t.Run("default", func(t *testing.T) { + t.Setenv("ENGRAM_MINIMAX_MODEL", "") + if got := miniMaxResolveModel(); got != MiniMaxDefaultModel { + t.Errorf("model = %q; want %q", got, MiniMaxDefaultModel) + } + }) + t.Run("override", func(t *testing.T) { + t.Setenv("ENGRAM_MINIMAX_MODEL", MiniMaxModelM27) + if got := miniMaxResolveModel(); got != MiniMaxModelM27 { + t.Errorf("model = %q; want %q", got, MiniMaxModelM27) + } + }) +}