-
Notifications
You must be signed in to change notification settings - Fork 619
Add MiniMax runner to the semantic-comparison factory #682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
octo-patch
wants to merge
1
commit into
Gentleman-Programming:main
Choose a base branch
from
octo-patch:octo/20260729-provider-add-recvq9LEXlx7Ts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
No validation of resolved model/region against known constants.
miniMaxResolveModelaccepts anyENGRAM_MINIMAX_MODELvalue without validating it againstMiniMaxModelIDs, andminiMaxResolveBaseURL's region switch silently falls back to the global endpoint on an unrecognizedENGRAM_MINIMAX_REGIONvalue (e.g. a typo). Misconfiguration will only surface as an opaque API error at request time rather than a clear startup/config error.♻️ Suggested validation
func miniMaxResolveModel() string { if v := strings.TrimSpace(os.Getenv("ENGRAM_MINIMAX_MODEL")); v != "" { + for _, id := range MiniMaxModelIDs { + if v == id { + return v + } + } + // fall through with a clear signal that the override isn't a known model return v } return MiniMaxDefaultModel }🤖 Prompt for AI Agents