diff --git a/docs/reference/g14-gitops-cicd.md b/docs/reference/g14-gitops-cicd.md index 71e340e6..903571cd 100644 --- a/docs/reference/g14-gitops-cicd.md +++ b/docs/reference/g14-gitops-cicd.md @@ -117,8 +117,9 @@ G14 Code Agent 通过同一个 repo-owned Codex app-server stdio runner 承载 | --- | --- | --- | --- | --- | | `deepseek` | 是 | `HWLAB_CODE_AGENT_DEEPSEEK_MODEL`,默认 `deepseek-chat` | `HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL`,默认 `http://hwlab-deepseek-proxy..svc.cluster.local:4000/v1/responses` | G14 集群内 DeepSeek Responses bridge;Service 4000 先进入 `hwlab-deepseek-responses-bridge`,再转发到同 Pod 内 Moon Bridge 4001。 | | `codex-api` | 否 | `HWLAB_CODE_AGENT_CODEX_API_MODEL`,默认 `gpt-5.5` | `HWLAB_CODE_AGENT_CODEX_API_BASE_URL`,G14 默认应指向同 Pod `127.0.0.1` loopback forwarder,forwarder upstream 为 hyueapi | 独立 Codex/OpenAI-compatible Responses API 通道;不得依赖 DeepSeek bridge。 | +| `minimax-m3` | 否 | `HWLAB_CODE_AGENT_MINIMAX_M3_MODEL`,默认 `MiniMax-M3` | 不由 HWLAB 配置;由 AgentRun `backendProfile=minimax-m3` 的 Secret/profile 承接 | HWLAB 只把该 profile 委托给 AgentRun v0.1,不在 HWLAB 新增 MiniMax API key、base URL、proxy 或 provider 实现。 | -`hwlab-cloud-api` 仍以 `HWLAB_CODE_AGENT_PROVIDER=codex-stdio` 运行。`HWLAB_CODE_AGENT_MODEL` 与 `HWLAB_CODE_AGENT_OPENAI_BASE_URL` 可作为 runtime-default 兜底,但前端默认 profile 是 `deepseek`,用户可以切到 `codex-api`。Codex app-server 启动参数必须同时设置 provider base URL、provider `name`、`model` 和 `review_model`,否则 Codex/DeepSeek 链路可能在模型目录或 `/v1/responses` 阶段退化成 `model=None`。G14 cloud-api 的 `NO_PROXY/no_proxy` 必须包含 `hyueapi.com` 和 `.hyueapi.com`;`codex-api` 的 hyueapi upstream 由同 Pod loopback forwarder 直连,不能被 proxy 注入污染。 +`hwlab-cloud-api` 仍以 `HWLAB_CODE_AGENT_PROVIDER=codex-stdio` 运行。`HWLAB_CODE_AGENT_MODEL` 与 `HWLAB_CODE_AGENT_OPENAI_BASE_URL` 可作为 runtime-default 兜底,但前端默认 profile 是 `deepseek`,用户可以切到 `codex-api` 或 `minimax-m3`。Codex app-server 启动参数必须同时设置 provider base URL、provider `name`、`model` 和 `review_model`,否则 Codex/DeepSeek 链路可能在模型目录或 `/v1/responses` 阶段退化成 `model=None`。`minimax-m3` 是 AgentRun 委托 profile,HWLAB 只负责把用户选择透传为 AgentRun `backendProfile=minimax-m3`,不得为了该 profile 在 HWLAB 里重新读取 MiniMax Secret 或维护第二套 provider bridge。G14 cloud-api 的 `NO_PROXY/no_proxy` 必须包含 `hyueapi.com` 和 `.hyueapi.com`;`codex-api` 的 hyueapi upstream 由同 Pod loopback forwarder 直连,不能被 proxy 注入污染。 G14 `codex-api` 不得把 `http://172.26.26.227:17680/v1/responses` 作为默认 base URL;该地址只保留为 D601 legacy Code Queue runner 或历史 egress 对照线索。G14 上的 `codex-api` 应先进入同 Pod `127.0.0.1` loopback forwarder,再由 forwarder 直连 `hyueapi.com` / `.hyueapi.com`;这两个域名必须同时进入 `NO_PROXY` 和 `no_proxy`。DeepSeek profile 可以通过集群内 bridge/Moon Bridge 转换,`codex-api` 不能用 DeepSeek bridge 伪装通过,也不能因为默认 profile 切到 DeepSeek 而删除或覆盖 `codex-api` 的独立模型、base URL、auth 和最小闭环验证。 diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 46f672cd..c5b911ac 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -29,7 +29,7 @@ const DEFAULT_PROVIDER_ID = "G14"; const DEFAULT_REPO_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git"; const DEFAULT_RUNNER_NAMESPACE = "agentrun-v01"; const DEFAULT_TIMEOUT_MS = 1_200_000; -const AGENTRUN_BACKENDS = Object.freeze(["codex", "deepseek"]); +const AGENTRUN_BACKENDS = Object.freeze(["codex", "deepseek", "minimax-m3"]); const THREAD_CONTINUITY_POLICY = "hwlab-agentrun-v01-reuse-runner-thread"; const SESSION_POLICY_RUN_LOCAL = "hwlab-agentrun-v01-session-runner-reuse"; const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "blocked", "cancelled", "canceled"]); @@ -603,11 +603,12 @@ function runLeaseExpired(leaseExpiresAt) { } function providerCredentialSecretRef(profile, env) { + const profileEnvKey = String(profile ?? "").toUpperCase().replace(/[^A-Z0-9]+/gu, "_"); return { profile, secretRef: { namespace: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE, DEFAULT_RUNNER_NAMESPACE), - name: firstNonEmpty(env[`HWLAB_CODE_AGENT_AGENTRUN_${profile.toUpperCase()}_SECRET_NAME`], env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_SECRET_NAME, `agentrun-v01-provider-${profile}`), + name: firstNonEmpty(env[`HWLAB_CODE_AGENT_AGENTRUN_${profileEnvKey}_SECRET_NAME`], env[`HWLAB_CODE_AGENT_AGENTRUN_${String(profile ?? "").toUpperCase()}_SECRET_NAME`], env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_SECRET_NAME, `agentrun-v01-provider-${profile}`), keys: ["auth.json", "config.toml"], mountPath: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_CODEX_HOME_MOUNT, "~/.codex") } @@ -1120,6 +1121,7 @@ function fullSourceCommit(env) { function modelForBackendProfile(profile, env = process.env) { if (profile === "codex") return firstNonEmpty(env.HWLAB_CODE_AGENT_CODEX_API_MODEL, env.HWLAB_CODE_AGENT_MODEL, "gpt-5.5"); + if (profile === "minimax-m3") return firstNonEmpty(env.HWLAB_CODE_AGENT_MINIMAX_M3_MODEL, "MiniMax-M3"); return firstNonEmpty(env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL, "deepseek-chat"); } diff --git a/internal/cloud/server-agent-chat.test.ts b/internal/cloud/server-agent-chat.test.ts index 0dc41cab..1eba39e0 100644 --- a/internal/cloud/server-agent-chat.test.ts +++ b/internal/cloud/server-agent-chat.test.ts @@ -410,6 +410,127 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte } }); +test("cloud api AgentRun adapter maps minimax-m3 provider profile to AgentRun backend", async () => { + const calls = []; + const agentRunServer = createHttpServer(async (request, response) => { + const url = new URL(request.url || "/", "http://127.0.0.1"); + const chunks = []; + for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : null; + calls.push({ method: request.method, path: url.pathname, body }); + const send = (data) => { + response.writeHead(200, { "content-type": "application/json" }); + response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_agentrun_minimax_m3" })}\n`); + }; + if (request.method === "POST" && url.pathname === "/api/v1/runs") { + assert.equal(body.backendProfile, "minimax-m3"); + assert.equal(body.executionPolicy.secretScope.providerCredentials[0].profile, "minimax-m3"); + assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.name, "agentrun-v01-provider-minimax-m3"); + assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.namespace, "agentrun-v01"); + return send({ id: "run_hwlab_minimax_m3", status: "pending", backendProfile: "minimax-m3", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef }); + } + if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/commands") { + assert.equal(body.payload.providerProfile, "minimax-m3"); + assert.match(body.payload.prompt, /MiniMax-M3/u); + return send({ id: "cmd_hwlab_minimax_m3", runId: "run_hwlab_minimax_m3", state: "pending", type: "turn", seq: 1 }); + } + if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/runner-jobs") { + assert.equal(body.commandId, "cmd_hwlab_minimax_m3"); + return send({ + action: "create-kubernetes-job", + runId: "run_hwlab_minimax_m3", + commandId: "cmd_hwlab_minimax_m3", + attemptId: "attempt_hwlab_minimax_m3", + runnerId: "runner_hwlab_minimax_m3", + namespace: "agentrun-v01", + jobName: "agentrun-v01-runner-hwlab-minimax-m3", + jobIdentity: { namespace: "agentrun-v01", name: "agentrun-v01-runner-hwlab-minimax-m3" }, + runner: { attemptId: "attempt_hwlab_minimax_m3", runnerId: "runner_hwlab_minimax_m3" } + }); + } + if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/events") { + return send({ items: [ + { id: "evt_minimax_1", runId: "run_hwlab_minimax_m3", seq: 1, type: "backend_status", payload: { phase: "runner-job-created", commandId: "cmd_hwlab_minimax_m3", attemptId: "attempt_hwlab_minimax_m3", jobName: "agentrun-v01-runner-hwlab-minimax-m3", namespace: "agentrun-v01" }, createdAt: "2026-06-02T00:00:00.000Z" }, + { id: "evt_minimax_2", runId: "run_hwlab_minimax_m3", seq: 2, type: "assistant_message", payload: { commandId: "cmd_hwlab_minimax_m3", text: "AGENTRUN_MINIMAX_M3_OK" }, createdAt: "2026-06-02T00:00:01.000Z" }, + { id: "evt_minimax_3", runId: "run_hwlab_minimax_m3", seq: 3, type: "terminal_status", payload: { commandId: "cmd_hwlab_minimax_m3", terminalStatus: "completed" }, createdAt: "2026-06-02T00:00:02.000Z" } + ] }); + } + if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/commands/cmd_hwlab_minimax_m3/result") { + return send({ + runId: "run_hwlab_minimax_m3", + commandId: "cmd_hwlab_minimax_m3", + attemptId: "attempt_hwlab_minimax_m3", + runnerId: "runner_hwlab_minimax_m3", + jobName: "agentrun-v01-runner-hwlab-minimax-m3", + namespace: "agentrun-v01", + status: "completed", + runStatus: "completed", + commandState: "completed", + terminalStatus: "completed", + completed: true, + reply: "AGENTRUN_MINIMAX_M3_OK", + lastSeq: 3, + eventCount: 3, + sessionRef: { sessionId: "ses_server-test-minimax-m3", conversationId: "cnv_server-test-minimax-m3", threadId: null } + }); + } + response.writeHead(404, { "content-type": "application/json" }); + response.end(`${JSON.stringify({ ok: false, failureKind: "schema-invalid", message: `unexpected ${request.method} ${url.pathname}`, traceId: "trc_fake_agentrun_minimax_m3" })}\n`); + }); + await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve)); + + const agentRunPort = agentRunServer.address().port; + const server = createCloudApiServer({ + env: { + HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01", + AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`, + HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1", + HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567", + HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek", + HWLAB_ENVIRONMENT: "v02", + HWLAB_GITOPS_PROFILE: "v02" + }, + accessController: { + required: false, + async authenticate() { + return { ok: true, actor: null, session: null }; + } + } + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const { port } = server.address(); + const traceId = "trc_server-test-agentrun-minimax-m3"; + const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, { + method: "POST", + headers: { "content-type": "application/json", "x-trace-id": traceId }, + body: JSON.stringify({ + conversationId: "cnv_server-test-minimax-m3", + sessionId: "ses_server-test-minimax-m3", + providerProfile: "minimax-m3", + message: "MiniMax-M3 AgentRun adapter smoke" + }) + }); + assert.equal(submit.status, 202); + const accepted = await submit.json(); + assert.equal(accepted.shortConnection, true); + + const payload = await pollAgentResult(port, traceId); + validateCodeAgentChatSchema(payload); + assert.equal(payload.status, "completed"); + assert.equal(payload.infrastructureBackend, "agentrun-v01/minimax-m3"); + assert.equal(payload.agentRun.backendProfile, "minimax-m3"); + assert.equal(payload.providerTrace.backendProfile, "minimax-m3"); + assert.equal(payload.agentRun.jobName, "agentrun-v01-runner-hwlab-minimax-m3"); + assert.equal(payload.reply.content, "AGENTRUN_MINIMAX_M3_OK"); + assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs").length, 1); + } finally { + await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); + await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve()))); + } +}); + test("cloud api AgentRun adapter rejects non-internal manager URLs by default", async () => { const server = createCloudApiServer({ env: { diff --git a/internal/cloud/server-code-agent-http.ts b/internal/cloud/server-code-agent-http.ts index c4b7a723..6a21541d 100644 --- a/internal/cloud/server-code-agent-http.ts +++ b/internal/cloud/server-code-agent-http.ts @@ -31,15 +31,17 @@ import { const DEFAULT_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT = 120; const DEFAULT_CODE_AGENT_PROVIDER_PROFILE = "deepseek"; -const CODE_AGENT_PROVIDER_PROFILE_IDS = Object.freeze(["deepseek", "codex-api", "runtime-default"]); +const CODE_AGENT_PROVIDER_PROFILE_IDS = Object.freeze(["deepseek", "codex-api", "minimax-m3", "runtime-default"]); const CODE_AGENT_PROVIDER_PROFILE_LABELS = Object.freeze({ "deepseek": "DeepSeek", "codex-api": "Codex API", + "minimax-m3": "MiniMax-M3 via AgentRun", "runtime-default": "运行默认" }); const DEFAULT_CODE_AGENT_CODEX_API_MODEL = "gpt-5.5"; const DEFAULT_CODE_AGENT_CODEX_API_BASE_URL = "http://127.0.0.1:49280/responses"; const DEFAULT_CODE_AGENT_DEEPSEEK_MODEL = "deepseek-chat"; +const DEFAULT_CODE_AGENT_MINIMAX_M3_MODEL = "MiniMax-M3"; export async function handleCodeAgentChatHttp(request, response, options) { const body = await readBody(request, options.bodyLimitBytes); @@ -252,6 +254,8 @@ function codeAgentProviderProfileEnv(env = process.env, params = {}) { if (profile === "codex-api") { overlay.HWLAB_CODE_AGENT_MODEL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_CODEX_API_MODEL, env.HWLAB_CODE_AGENT_LEGACY_CODEX_MODEL, DEFAULT_CODE_AGENT_CODEX_API_MODEL); overlay.HWLAB_CODE_AGENT_OPENAI_BASE_URL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_CODEX_API_BASE_URL, env.HWLAB_CODE_AGENT_LEGACY_OPENAI_BASE_URL, DEFAULT_CODE_AGENT_CODEX_API_BASE_URL); + } else if (profile === "minimax-m3") { + overlay.HWLAB_CODE_AGENT_MODEL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_MINIMAX_M3_MODEL, DEFAULT_CODE_AGENT_MINIMAX_M3_MODEL); } else { overlay.HWLAB_CODE_AGENT_MODEL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL, DEFAULT_CODE_AGENT_DEEPSEEK_MODEL); overlay.HWLAB_CODE_AGENT_OPENAI_BASE_URL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL, defaultDeepSeekBaseUrlForEnv(env)); diff --git a/tools/src/hwlab-cli-lib.ts b/tools/src/hwlab-cli-lib.ts index cedf32b7..00f37f8b 100644 --- a/tools/src/hwlab-cli-lib.ts +++ b/tools/src/hwlab-cli-lib.ts @@ -123,13 +123,13 @@ function help() { "hwlab-cli client workbench reset --profile NAME --confirm", "hwlab-cli client request GET /v1/access/status [--full]", "hwlab-cli client rpc system.health [--full]", - "hwlab-cli client agent send --message TEXT|--message-file PATH [--from-trace TRACE] [--conversation-id ID] [--session-id ID] [--thread-id ID] [--retry-of TRACE] --provider-profile deepseek", + "hwlab-cli client agent send --message TEXT|--message-file PATH [--from-trace TRACE] [--conversation-id ID] [--session-id ID] [--thread-id ID] [--retry-of TRACE] --provider-profile deepseek|codex-api|minimax-m3", "hwlab-cli client agent send --message TEXT --wait --timeout-ms 50000", "hwlab-cli client agent result TRACE_ID", "hwlab-cli client agent trace TRACE_ID [--render web]", "hwlab-cli client agent inspect --trace-id TRACE_ID", "hwlab-cli client agent cancel TRACE_ID", - "hwlab-cli client harness submit --message TEXT --provider-profile deepseek", + "hwlab-cli client harness submit --message TEXT --provider-profile deepseek|codex-api|minimax-m3", "hwlab-cli client harness wait TRACE_ID --timeout-ms 50000", "hwlab-cli client harness audit TRACE_ID --require-bootsharp" ] @@ -716,7 +716,7 @@ function harnessHelp() { aliases: ["harness", "harness-ops", "harness-opt"], commands: [ "health [--base-url URL]", - "submit --message TEXT|--message-file PATH [--conversation-id ID] [--trace-id ID] [--provider-profile deepseek|codex-api]", + "submit --message TEXT|--message-file PATH [--conversation-id ID] [--trace-id ID] [--provider-profile deepseek|codex-api|minimax-m3]", "result TRACE_ID [--full]", "trace TRACE_ID [--limit N] [--full]", "wait TRACE_ID [--timeout-ms N] [--poll-interval-ms N]", diff --git a/web/hwlab-cloud-web/app.ts b/web/hwlab-cloud-web/app.ts index 61bb988d..9b1631c5 100644 --- a/web/hwlab-cloud-web/app.ts +++ b/web/hwlab-cloud-web/app.ts @@ -23,7 +23,7 @@ const CODE_AGENT_TIMEOUT_STORAGE_KEY = "hwlab.workbench.codeAgentTimeoutMs.v1"; const GATEWAY_SHELL_TIMEOUT_STORAGE_KEY = "hwlab.workbench.gatewayShellTimeoutMs.v1"; const CODE_AGENT_PROVIDER_PROFILE_STORAGE_KEY = "hwlab.workbench.codeAgentProviderProfile.v1"; const DEFAULT_CODE_AGENT_PROVIDER_PROFILE = "deepseek"; -const CODE_AGENT_PROVIDER_PROFILES = Object.freeze(["deepseek", "codex-api"]); +const CODE_AGENT_PROVIDER_PROFILES = Object.freeze(["deepseek", "codex-api", "minimax-m3"]); const CODE_AGENT_SESSION_STORAGE_KEY = "hwlab.workbench.codeAgentSession.v1"; const CODE_AGENT_SESSION_STORAGE_VERSION = 1; const CODE_AGENT_SESSION_STORAGE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; @@ -396,7 +396,9 @@ function syncCodeAgentTimeoutControl() { el.codeAgentProviderProfile.value = CODE_AGENT_PROVIDER_PROFILE; el.codeAgentProviderProfile.title = CODE_AGENT_PROVIDER_PROFILE === "codex-api" ? "本次 Code Agent 请求使用原 Codex API / Responses 通道" - : "本次 Code Agent 请求使用 G14 DeepSeek Responses bridge"; + : CODE_AGENT_PROVIDER_PROFILE === "minimax-m3" + ? "本次 Code Agent 请求通过 AgentRun MiniMax-M3 backend profile 执行" + : "本次 Code Agent 请求使用 G14 DeepSeek Responses bridge"; el.codeAgentTimeout.value = String(CODE_AGENT_TIMEOUT_MS); el.codeAgentTimeout.title = `Code Agent 无新事件超过 ${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 后才显示为超时`; el.gatewayShellTimeout.value = String(GATEWAY_SHELL_TIMEOUT_MS); diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 0c56e57f..1d244974 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -195,6 +195,7 @@