From 37cd6e97c17c5eeb1cd99d601d2aaff75c573a88 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 08:33:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(minimax-code):=20effort=20xhigh,=20permiss?= =?UTF-8?q?ion=20full,=20=EB=AA=A8=EB=8D=B8=20=ED=95=9C=EB=8F=84=C2=B7thin?= =?UTF-8?q?king=20=EC=A0=95=EC=9D=98=20(AC-G2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mcode exec 에 --effort xhigh 를 고정하고 권한을 smart → full 로 바꾼다. smart 는 사람 없이 대부분의 셸 명령을 거부해 무인 실행 turn 을 실패시킨다. - 관리형 설정의 모델 정의를 mcode 내장 M3 계열 정의에 맞춘다: effortOptions (없으면 mcode 가 --effort 를 거부), thinking_config forced_on, variants, 그리고 MiniMax-M3.1-Flash 의 limit 512k/128k (없으면 max_tokens 16384). - 캡처 서버로 확인한 실제 요청: max_tokens 128000, thinking adaptive, output_config.effort xhigh. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R1VrPpZUiPjHiu3NHL7jrA --- data-plane/README.md | 7 ++- data-plane/cmd/agent/minimax_code.go | 48 +++++++++++++++++-- data-plane/cmd/agent/minimax_code_test.go | 56 ++++++++++++++++++++++- docs/prd/minimax-code-workload.md | 6 +-- 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/data-plane/README.md b/data-plane/README.md index ab4e587..6414602 100644 --- a/data-plane/README.md +++ b/data-plane/README.md @@ -48,13 +48,16 @@ interactive shell and Claude Code workloads. `claude-code` — queue, bounds, cursor, resume flag and archive are shared (AC-G2/G4). Each job executes `mcode exec --config CONFIG --model custom_provider:session-platform/MODEL - --permission smart --output-format stream-json [--continue] -- PROMPT`, and + --permission full --effort xhigh --output-format stream-json [--continue] -- + PROMPT`, and the agent projects only the `contentDelta` text of `agent_message` items; reasoning, tool-call and lifecycle records are dropped, and a failed turn is reported through mcode's own stderr diagnostics. `CONFIG` is a platform-managed file written at startup *beside* the state tree (so it is never archived): it declares one Anthropic-messages provider at `http://127.0.0.1:8091` with the - non-secret placeholder key and the session's model, and turns data + non-secret placeholder key and the session's model (with the effort levels + mcode needs to accept `--effort`, and the 512k/128k token limits for + `MiniMax-M3.1-Flash`), and turns data contribution and self-update off; the agent also sets `MCODE_DISABLE_EVAL_CAPTURE=1` so no run uploads step reports. `MINIMAX_CODE_MODEL` selects the model; `platform-default` or empty runs `MiniMax-M3.1-Flash`, because mcode, unlike Claude Code, has no diff --git a/data-plane/cmd/agent/minimax_code.go b/data-plane/cmd/agent/minimax_code.go index 242bbae..47e9dbe 100644 --- a/data-plane/cmd/agent/minimax_code.go +++ b/data-plane/cmd/agent/minimax_code.go @@ -32,9 +32,15 @@ const ( // miniMaxProviderID names the one provider entry the managed config // declares. The CLI addresses models as custom_provider:/. miniMaxProviderID = "session-platform" - // miniMaxPermission is mcode's headless default, pinned so an upstream - // default change cannot silently widen what an unattended run may do. - miniMaxPermission = "smart" + // miniMaxPermission is pinned, not left to mcode's default. A run is + // unattended, so a mode that stops to ask (smart refuses most shell + // commands without a human) only fails the turn. What bounds the tools is + // the session pod, which never holds a real credential (AC-G3). + miniMaxPermission = "full" + // miniMaxEffort is the reasoning effort every run asks for. mcode sends it + // as output_config.effort, and only for a model whose definition lists + // effort options — which is why the managed config always declares them. + miniMaxEffort = "xhigh" // The managed config lives beside the state tree, not in it: it is // regenerated by every pod from that pod's own environment, so it never @@ -91,6 +97,7 @@ func (m miniMaxCLI) argv(binary, model, prompt string, resume bool) []string { "--config", m.configPath, "--model", "custom_provider:" + miniMaxProviderID + "/" + resolveMiniMaxModel(model), "--permission", miniMaxPermission, + "--effort", miniMaxEffort, "--output-format", "stream-json", } if resume { @@ -140,7 +147,7 @@ func (m miniMaxCLI) renderConfig() ([]byte, error) { "apiKey": m.placeholder, }, "models": map[string]any{ - m.model: map[string]any{"reasoning": true}, + m.model: miniMaxModelDefinition(m.model), }, }, }, @@ -152,6 +159,39 @@ func (m miniMaxCLI) renderConfig() ([]byte, error) { return append(out, '\n'), nil } +// miniMaxModelLimits are the token limits of the models mcode itself ships a +// definition for. A custom provider entry does not inherit them: without a +// limit, mcode assumes a 200k context and caps every response at 16384 +// tokens, which truncates long edits and starves the thinking budget. +var miniMaxModelLimits = map[string]map[string]any{ + "MiniMax-M3.1-Flash": {"context": 512000, "output": 128000}, +} + +// miniMaxModelDefinition mirrors mcode's built-in definition of the MiniMax +// M3 family: adaptive thinking that is always on, and the effort levels the +// --effort flag may select. Limits are added only for a model they are known +// for; any other model keeps mcode's conservative fallback rather than a +// guessed window the endpoint would reject. +func miniMaxModelDefinition(model string) map[string]any { + def := map[string]any{ + "reasoning": true, + "tool_call": true, + "thinking": map[string]any{ + "effortOptions": []string{"default", "low", "medium", "high", "xhigh", "max"}, + "defaultEffort": "default", + }, + "thinking_config": map[string]any{"mode": "forced_on"}, + "variants": map[string]any{ + "none-thinking": map[string]any{"thinking": map[string]any{"type": "disabled"}}, + "thinking": map[string]any{"thinking": map[string]any{"type": "adaptive"}}, + }, + } + if limit, ok := miniMaxModelLimits[model]; ok { + def["limit"] = limit + } + return def +} + func writeMiniMaxConfig(path string, content []byte) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o700); err != nil { diff --git a/data-plane/cmd/agent/minimax_code_test.go b/data-plane/cmd/agent/minimax_code_test.go index a575593..4ce9ab0 100644 --- a/data-plane/cmd/agent/minimax_code_test.go +++ b/data-plane/cmd/agent/minimax_code_test.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "testing" ) @@ -43,7 +44,7 @@ func newMiniMaxTestServerAt(t *testing.T, runner commandRunner, model string, re func miniMaxArgv(cli miniMaxCLI, model string, resume bool, prompt string) []string { argv := []string{"mcode", "exec", "--config", cli.configPath, "--model", "custom_provider:session-platform/" + model, - "--permission", "smart", "--output-format", "stream-json"} + "--permission", "full", "--effort", "xhigh", "--output-format", "stream-json"} if resume { argv = append(argv, "--continue") } @@ -153,6 +154,59 @@ func TestMiniMaxManagedConfig(t *testing.T) { } } +type miniMaxModelShape struct { + Reasoning bool `json:"reasoning"` + Thinking struct { + EffortOptions []string `json:"effortOptions"` + } `json:"thinking"` + ThinkingConfig struct { + Mode string `json:"mode"` + } `json:"thinking_config"` + Limit *struct { + Context int `json:"context"` + Output int `json:"output"` + } `json:"limit"` +} + +// AC-G2: mcode refuses --effort for a model whose definition lists no effort +// options, and without a limit it caps every response at 16384 tokens. The +// managed config therefore declares the effort levels for any session model, +// and the real token limits for the models they are known for. +func TestMiniMaxManagedModelDefinition(t *testing.T) { + for _, tc := range []struct { + model string + context, output int + wantsKnownLimits bool + }{ + {model: miniMaxDefaultModel, context: 512000, output: 128000, wantsKnownLimits: true}, + {model: "MiniMax-M2.7"}, + } { + t.Run(tc.model, func(t *testing.T) { + _, cli, _ := newMiniMaxTestServerAt(t, newFakeClaudeRunner(), tc.model, false, filepath.Join(t.TempDir(), "state")) + raw, ok := readMiniMaxConfig(t, cli.configPath).CustomProvider["session-platform"].Models[tc.model] + if !ok { + t.Fatalf("managed config does not declare %s", tc.model) + } + var model miniMaxModelShape + if err := json.Unmarshal(raw, &model); err != nil { + t.Fatalf("decode model definition: %v\n%s", err, raw) + } + if !model.Reasoning || model.ThinkingConfig.Mode != "forced_on" { + t.Fatalf("model %s must reason with thinking forced on: %s", tc.model, raw) + } + if !slices.Contains(model.Thinking.EffortOptions, miniMaxEffort) { + t.Fatalf("effort options %q do not offer the pinned effort %q", model.Thinking.EffortOptions, miniMaxEffort) + } + switch { + case tc.wantsKnownLimits && (model.Limit == nil || model.Limit.Context != tc.context || model.Limit.Output != tc.output): + t.Fatalf("limit = %s, want context %d output %d", raw, tc.context, tc.output) + case !tc.wantsKnownLimits && model.Limit != nil: + t.Fatalf("model %s must keep mcode's fallback limits, got %+v", tc.model, *model.Limit) + } + }) + } +} + // AC-G2: only assistant message text reaches the session output. Reasoning, // tool calls and lifecycle records are dropped, deltas are not repeated by the // completed record, and consecutive messages end on their own lines. diff --git a/docs/prd/minimax-code-workload.md b/docs/prd/minimax-code-workload.md index 33e19c8..79c3147 100644 --- a/docs/prd/minimax-code-workload.md +++ b/docs/prd/minimax-code-workload.md @@ -34,13 +34,13 @@ ``` mcode exec --config <관리형 설정> \ --model custom_provider:session-platform/<모델> \ - --permission smart --output-format stream-json [--continue] -- "" + --permission full --effort xhigh --output-format stream-json [--continue] -- "" ``` - `--` 뒤 payload는 옵션이 아닌 단일 positional argv다. 권한은 `mcode`의 비대화 기본값 `smart`로 **고정**한다 — 상위 버전의 기본값 변경이 무인 실행의 권한을 조용히 넓히지 못하게 하기 위해서다. data plane은 stdout JSONL에서 `agent_message` 항목의 `contentDelta`만 순서대로 투영하고, 항목이 끝날 때(`item.completed`) 줄바꿈을 맞춘다. 델타 없이 끝난 항목은 완료 레코드의 `content`로 한 번만 투영한다. reasoning·tool_call·생명주기 레코드는 사용자 출력이 아니다. 실패한 turn은 `mcode`가 stderr에도 보고하므로 별도로 투영하지 않으며, stderr는 AC-E3대로 진단 출력으로 같은 버퍼에 합쳐진다. + `--` 뒤 payload는 옵션이 아닌 단일 positional argv다. 권한은 `full`, 추론 강도는 `xhigh`로 **고정**한다 — 실행은 무인이므로 사람에게 묻고 멈추는 모드(`smart`는 사람 없이 대부분의 셸 명령을 거부한다)는 turn을 실패시킬 뿐이고, 도구의 경계는 실제 자격 증명을 갖지 않는 세션 파드다(AC-G3). 고정값이므로 상위 버전의 기본값 변경이 실행 방식을 조용히 바꾸지 못한다. `mcode`는 모델 정의에 추론 강도 선택지가 없으면 `--effort`를 거부하고, 토큰 한도가 없으면 응답을 16384 토큰으로 자르므로, 관리형 설정은 세션 모델에 추론 강도 선택지(항상 켜진 adaptive thinking)를 선언하고 한도가 알려진 모델(`MiniMax-M3.1-Flash`: 컨텍스트 512k·출력 128k)에는 그 한도도 선언한다. data plane은 stdout JSONL에서 `agent_message` 항목의 `contentDelta`만 순서대로 투영하고, 항목이 끝날 때(`item.completed`) 줄바꿈을 맞춘다. 델타 없이 끝난 항목은 완료 레코드의 `content`로 한 번만 투영한다. reasoning·tool_call·생명주기 레코드는 사용자 출력이 아니다. 실패한 turn은 `mcode`가 stderr에도 보고하므로 별도로 투영하지 않으며, stderr는 AC-E3대로 진단 출력으로 같은 버퍼에 합쳐진다. - **달성 가치**: V3, V4 - **구체화 대상**: AC-C2(read)·AC-C3(write) — AC-E2/E3의 투영 규칙을 이 CLI의 스트림 형식에 맞춰 바꾼 것 -- **검증 방법**: 첫 write의 argv에 `--continue`가 없고 성공 후 두 번째 write에는 있음을 확인한다. `--`로 시작하는 프롬프트가 옵션으로 해석되지 않음을 확인한다. reasoning·tool_call이 섞인 스트림에서 assistant 텍스트만, 중복 없이, 메시지마다 줄바꿈되어 투영됨을 확인한다. +- **검증 방법**: 모든 write의 argv에 `--permission full --effort xhigh`가 있고, 관리형 설정의 세션 모델 정의가 `xhigh`를 추론 강도 선택지로 담으며 `MiniMax-M3.1-Flash`에는 512k/128k 한도를 담음을 확인한다. 첫 write의 argv에 `--continue`가 없고 성공 후 두 번째 write에는 있음을 확인한다. `--`로 시작하는 프롬프트가 옵션으로 해석되지 않음을 확인한다. reasoning·tool_call이 섞인 스트림에서 assistant 텍스트만, 중복 없이, 메시지마다 줄바꿈되어 투영됨을 확인한다. ### AC-G3: 공급자 자격 증명은 이 타입 전용 Secret, 배치는 AC-E6 사이드카 - **설명**: 플랫폼 Secret `minimax-code-credentials`(이름은 control plane 설정으로 바꿀 수 있다)의 키 이름과 의미는 AC-E6과 같다 — 필수 `base-url`·`auth-token`은 credential-proxy 사이드카에만, 선택 `ca-cert`도 사이드카에만, 선택 `model`은 `platform-default` 세션의 주 컨테이너에만 주입된다. `base-url`은 MiniMax의 Anthropic 호환 엔드포인트(예: `https://api.minimax.io/anthropic`)이고 `auth-token`은 Token Plan **Subscription Key** 또는 종량제 API 키다. proxy는 AC-E6 그대로 HTTPS origin을 고정하고 허용 목록 헤더만 전달한 뒤 토큰을 `Authorization: Bearer`로 주입하는데, MiniMax는 두 종류의 키 모두 이 형식을 받는다. 주 컨테이너는 `ANTHROPIC_BASE_URL=http://127.0.0.1:8091`과 비밀이 아닌 placeholder만 보고, 시작 시 `ANTHROPIC_API_KEY`·`CLAUDE_CODE_OAUTH_TOKEN`·`MINIMAX_API_KEY`·`MCODE_PROVIDER_API_KEY`·`K3S_MCP_TOKEN` 등 두 CLI가 스스로 읽을 수 있는 자격 증명 변수가 하나라도 있으면 기동을 거부한다. control plane은 `minimax-code` Secret 이름이 `claude-code` Secret 이름과 같으면 기동을 거부한다. `claude-code`의 플러그인 부트스트랩(K3s MCP 토큰)은 이 타입에 없다. `mcode`가 읽는 설정은 data plane이 기동할 때마다 **상태 트리 바깥**에 다시 쓰는 관리형 파일 하나로, loopback proxy를 가리키는 Anthropic-messages 공급자 하나와 세션 모델만 선언하고 데이터 기여(data contribution)와 자가 업데이트를 끈다. agent는 `MCODE_DISABLE_EVAL_CAPTURE=1`도 설정해 실행 단계 보고 업로드를 막는다. 이 파일은 아카이브되지 않으므로 복원이 낡은 설정을 되살릴 수 없다. **두 타입의 Secret은 서로 섞이지 않는다** — `minimax-code` 파드의 어떤 컨테이너도 `claude-code` Secret을 참조하지 않는다.