feat(code-queue): register minimax-m3 alongside minimax-m2.7
Add minimax-m3 as a sibling model on the same path as the existing
minimax-m2.7 support. M3 is the new PROD default; M2.7 remains available
as a one-line rollback target via MINIMAX_MODEL.
Changes:
- code-agent/common.ts: add minimaxM3Model constant, include in
defaultCodeModels, normalize aliases (minimax-m3 / m3), route to
opencode port, sibling entry in codeModelProviderSourceContract.
- code-agent/opencode.ts: add M3 branch in openCodeModelId, register
both M2.7 and M3 in openCodeConfigContent, inject MINIMAX_M3_MODEL
in local/remote env, M3 branch in missingOpenCodeCredentialMessage.
- index.ts + types.ts: add minimaxM3Model to RuntimeConfig, read
MINIMAX_M3_MODEL env, propagate to remoteCodexEnvKeys.
- queue-api.ts: include minimaxM3Model in Pick for
codeModelProviderSourceContract callers.
- scripts/src/code-queue.ts: add minimaxM3SubmitModel, normalize/port
routing, default route recommendation now points to M3, M2.7 kept
as minimaxM2FallbackCandidate.
- scripts/src/docker.ts: default UNIDESK_CODE_QUEUE_MINIMAX_M3_MODEL
to MiniMax-M3.
- k8s yaml (D601 + G14): CODE_QUEUE_MODELS gains minimax-m3, three
deployments each set MINIMAX_MODEL=MiniMax-M3 and
MINIMAX_M3_MODEL=MiniMax-M3.
- docker-compose.d601.yml: same env defaults for local compose.
- docs/reference/{code-queue-supervision,microservices,
windows-passthrough}.md: update model contracts to M3 default +
M2.7 fallback.
- scripts/code-queue-submit-routing-contract-test.ts: low-risk
recommendation now asserts minimax-m3; registry asserts both M3
and M2.7 fallback in modelPorts/opencodeModels; provider source
contract test passes minimaxM3Model.
Refs: #189
This commit is contained in:
@@ -44,7 +44,7 @@ export function runCodeQueueSubmitRoutingContract(): JsonRecord {
|
||||
const lowRisk = codexSubmitRoutingRecommendationForTest(lowRiskPrompt);
|
||||
assertCondition(lowRisk.route === "minimax-opencode", "low-risk self-contained prompt should be a MiniMax candidate", lowRisk);
|
||||
assertCondition(lowRisk.recommendedRunner === "opencode", "MiniMax candidate should recommend OpenCode", lowRisk);
|
||||
assertCondition(lowRisk.recommendedModel === "minimax-m2.7", "MiniMax candidate should recommend minimax-m2.7", lowRisk);
|
||||
assertCondition(lowRisk.recommendedModel === "minimax-m3", "MiniMax candidate should recommend minimax-m3 as new default", lowRisk);
|
||||
assertCondition(asRecord(lowRisk.riskControls).promptSelfContained === true, "low-risk prompt should be self-contained", lowRisk);
|
||||
assertCondition(asRecord(lowRisk.riskControls).issueIsNotOnlySource === true, "issue must not be the only source", lowRisk);
|
||||
|
||||
@@ -73,22 +73,25 @@ export function runCodeQueueSubmitRoutingContract(): JsonRecord {
|
||||
assertCondition(externalProvider429.commanderAction === "wait-while-exponential-backoff-is-healthy", "429 policy should tell commander to wait on healthy backoff", policyContract);
|
||||
assertCondition(Array.isArray(externalProvider429.interveneWhen), "429 policy should expose intervention conditions", policyContract);
|
||||
|
||||
const registry = codexSubmitModelRegistryForTest(["gpt-5.5", "deepseek", "minimax-m2.7"]);
|
||||
const registry = codexSubmitModelRegistryForTest(["gpt-5.5", "deepseek", "minimax-m3", "minimax-m2.7"]);
|
||||
const modelPorts = asRecord(registry.modelPorts);
|
||||
assertCondition(modelPorts["deepseek-chat"] === "opencode", "modelPorts should route deepseek-chat to OpenCode", registry);
|
||||
assertCondition(modelPorts["minimax-m2.7"] === "opencode", "modelPorts should keep MiniMax on OpenCode", registry);
|
||||
assertCondition(modelPorts["minimax-m3"] === "opencode", "modelPorts should keep minimax-m3 on OpenCode", registry);
|
||||
assertCondition(modelPorts["minimax-m2.7"] === "opencode", "modelPorts should keep minimax-m2.7 on OpenCode as fallback", registry);
|
||||
assertCondition(modelPorts["gpt-5.5"] === "codex", "modelPorts should keep default GPT on Codex", registry);
|
||||
assertCondition(registry.opencodeModels.includes("deepseek-chat"), "opencodeModels should include deepseek-chat", registry);
|
||||
assertCondition(registry.opencodeModels.includes("minimax-m2.7"), "opencodeModels should include MiniMax", registry);
|
||||
assertCondition(registry.opencodeModels.includes("minimax-m3"), "opencodeModels should include minimax-m3", registry);
|
||||
assertCondition(registry.opencodeModels.includes("minimax-m2.7"), "opencodeModels should include minimax-m2.7 as fallback", registry);
|
||||
assertCondition(registry.codexModels.includes("gpt-5.5"), "codexModels should include default GPT", registry);
|
||||
const providerSource = codeModelProviderSourceContract({
|
||||
codeModels: ["gpt-5.5", "deepseek", "minimax-m2.7"],
|
||||
codeModels: ["gpt-5.5", "deepseek", "minimax-m3", "minimax-m2.7"],
|
||||
deepseekApiBase: "https://api.deepseek.example",
|
||||
deepseekApiKey: "ds-secret-must-not-print",
|
||||
deepseekModel: "deepseek-chat",
|
||||
minimaxApiBase: "https://api.minimax.example",
|
||||
minimaxApiKey: "",
|
||||
minimaxModel: "MiniMax-M2.7",
|
||||
minimaxM3Model: "MiniMax-M3",
|
||||
}, {
|
||||
DEEPSEEK_API_KEY: "ds-secret-must-not-print",
|
||||
DEEPSEEK_API_BASE: "https://api.deepseek.example",
|
||||
@@ -121,10 +124,10 @@ export function runCodeQueueSubmitRoutingContract(): JsonRecord {
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"low-risk self-contained prompts recommend minimax-m2.7/OpenCode",
|
||||
"low-risk self-contained prompts recommend minimax-m3/OpenCode (new default)",
|
||||
"runtime/core work recommends GPT-5.5/Codex",
|
||||
"medium bounded frontend work recommends deepseek-chat/OpenCode",
|
||||
"model registry maps deepseek-chat and minimax-m2.7 to OpenCode and GPT-5.5 to Codex",
|
||||
"model registry maps deepseek-chat, minimax-m3, and minimax-m2.7 fallback to OpenCode and GPT-5.5 to Codex",
|
||||
"model provider source contract exposes DeepSeek refs/presence without secret values",
|
||||
"dry-run policy contract exposes model-tier concurrency",
|
||||
"prod/restart/secret/DB work is commander-only",
|
||||
|
||||
@@ -51,6 +51,7 @@ const detailInitialPromptPreviewChars = 1200;
|
||||
const detailBasePromptPreviewChars = 800;
|
||||
const detailLastAssistantPreviewChars = 1200;
|
||||
const minimaxSubmitModel = "minimax-m2.7";
|
||||
const minimaxM3SubmitModel = "minimax-m3";
|
||||
const deepseekSubmitModel = "deepseek-chat";
|
||||
const gptSubmitModel = "gpt-5.5";
|
||||
const submitLockWaitMs = 60_000;
|
||||
@@ -1300,6 +1301,7 @@ function normalizeSubmitModel(value: string | null | undefined): string {
|
||||
const lower = raw.toLowerCase();
|
||||
const leaf = lower.includes("/") ? lower.split("/").at(-1) ?? lower : lower;
|
||||
if (leaf === minimaxSubmitModel || leaf === "m2.7") return minimaxSubmitModel;
|
||||
if (leaf === minimaxM3SubmitModel || leaf === "m3") return minimaxM3SubmitModel;
|
||||
if (leaf === deepseekSubmitModel || leaf === "deepseek") return deepseekSubmitModel;
|
||||
return raw;
|
||||
}
|
||||
@@ -1322,7 +1324,7 @@ function submitModelRegistry(models: string[] = sharedDefaultCodeModels): {
|
||||
function submitRunnerForModel(model: string | null | undefined): "opencode" | "codex" | null {
|
||||
const normalized = normalizeSubmitModel(model);
|
||||
if (normalized.length === 0) return null;
|
||||
return normalized === minimaxSubmitModel || normalized === deepseekSubmitModel ? "opencode" : "codex";
|
||||
return normalized === minimaxSubmitModel || normalized === minimaxM3SubmitModel || normalized === deepseekSubmitModel ? "opencode" : "codex";
|
||||
}
|
||||
|
||||
function regexEvidence(text: string, patterns: RegExp[], limit = 6): string[] {
|
||||
@@ -1574,11 +1576,17 @@ function submitPolicyContract(): SubmitRoutingRecommendation["policyContract"] {
|
||||
requiredGuards: ["self-contained prompt", "limited write scope", "contract/unit verification", "commander review"],
|
||||
},
|
||||
{
|
||||
model: minimaxSubmitModel,
|
||||
model: minimaxM3SubmitModel,
|
||||
runner: "opencode",
|
||||
taskRisk: "simple-low-risk",
|
||||
requiredGuards: ["issue is auxiliary only", "evidence required", "no prod/secrets/DB writes", "diff and test review"],
|
||||
},
|
||||
{
|
||||
model: minimaxSubmitModel,
|
||||
runner: "opencode",
|
||||
taskRisk: "simple-low-risk-fallback",
|
||||
requiredGuards: ["issue is auxiliary only", "evidence required", "no prod/secrets/DB writes", "diff and test review"],
|
||||
},
|
||||
],
|
||||
externalProvider429: {
|
||||
commanderAction: "wait-while-exponential-backoff-is-healthy",
|
||||
@@ -1741,15 +1749,15 @@ function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRouting
|
||||
} else if (promptSelfContained && issueIsNotOnlySource && evidenceRequiredByPrompt && lowRiskEvidence.length > 0) {
|
||||
route = "minimax-opencode";
|
||||
recommendedRunner = "opencode";
|
||||
recommendedModel = minimaxSubmitModel;
|
||||
recommendedModel = minimaxM3SubmitModel;
|
||||
confidence = "high";
|
||||
reason = "The prompt looks self-contained, low-risk, and asks for verifiable evidence; it is a MiniMax/OpenCode candidate if the runner smoke is currently green.";
|
||||
reason = "The prompt looks self-contained, low-risk, and asks for verifiable evidence; it is a MiniMax M3/OpenCode candidate if the runner smoke is currently green.";
|
||||
} else if (lowRiskEvidence.length > 0 && issueIsNotOnlySource && noProdRestartSecretOrDbWrite) {
|
||||
route = "minimax-opencode";
|
||||
recommendedRunner = "opencode";
|
||||
recommendedModel = minimaxSubmitModel;
|
||||
recommendedModel = minimaxM3SubmitModel;
|
||||
confidence = "medium";
|
||||
reason = "The prompt has low-risk signals, but the commander should tighten self-contained context and evidence requirements before relying on MiniMax.";
|
||||
reason = "The prompt has low-risk signals, but the commander should tighten self-contained context and evidence requirements before relying on MiniMax M3.";
|
||||
}
|
||||
|
||||
const explicitNote = model.length === 0
|
||||
@@ -7963,7 +7971,8 @@ function codexSubmitTask(args: string[]): unknown {
|
||||
},
|
||||
commands: {
|
||||
submitAsRequested: "remove --dry-run to submit exactly this payload",
|
||||
minimaxCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${minimaxSubmitModel} --dry-run`,
|
||||
minimaxCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${minimaxM3SubmitModel} --dry-run`,
|
||||
minimaxM2FallbackCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${minimaxSubmitModel} --dry-run`,
|
||||
deepseekCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${deepseekSubmitModel} --dry-run`,
|
||||
gptCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${gptSubmitModel} --dry-run`,
|
||||
},
|
||||
|
||||
@@ -241,6 +241,7 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
|
||||
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS") || "3",
|
||||
UNIDESK_CODE_QUEUE_MINIMAX_API_KEY: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_API_KEY") || runtimeSecret("MINIMAX_API_KEY"),
|
||||
UNIDESK_CODE_QUEUE_MINIMAX_MODEL: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_MODEL") || runtimeSecret("MINIMAX_MODEL") || "MiniMax-M2.7",
|
||||
UNIDESK_CODE_QUEUE_MINIMAX_M3_MODEL: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_M3_MODEL") || runtimeSecret("MINIMAX_M3_MODEL") || "MiniMax-M3",
|
||||
UNIDESK_CODE_QUEUE_MINIMAX_API_BASE: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_API_BASE") || runtimeSecret("MINIMAX_API_BASE") || "https://api.minimaxi.com/v1",
|
||||
UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS: runtimeSecretWithDefault("UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS", "90000", "60000"),
|
||||
UNIDESK_CODE_QUEUE_REMOTE_WORKDIR: codeQueueEnvValue("UNIDESK_CODE_QUEUE_REMOTE_WORKDIR", codeQueueRemoteWorkdir),
|
||||
|
||||
Reference in New Issue
Block a user