diff --git a/docs/reference/code-queue-supervision.md b/docs/reference/code-queue-supervision.md index ae8eb4f6..25004712 100644 --- a/docs/reference/code-queue-supervision.md +++ b/docs/reference/code-queue-supervision.md @@ -43,13 +43,13 @@ ## 模型和成本路由 -Code Queue 派单模型按成本、可信度和 blast radius 分层:GPT-5.5/Codex 处理高风险和复杂任务,DeepSeek/Codex 处理中等复杂度且边界清晰的任务,MiniMax/OpenCode 处理简单、低权限、可复核任务,生产重启、密钥、数据库手工写入和运行中任务控制保留给指挥官或人工。 +Code Queue 派单模型按成本、可信度和 blast radius 分层:GPT-5.5/Codex 处理高风险和复杂任务,DeepSeek/OpenCode 处理中等复杂度且边界清晰的任务,MiniMax/OpenCode 处理简单、低权限、可复核任务,生产重启、密钥、数据库手工写入和运行中任务控制保留给指挥官或人工。 当前提交合同由 `bun scripts/cli.ts codex submit` 暴露:prompt 必须来自位置参数、`--prompt-file` 或 `--prompt-stdin`;可选字段包括 `--queue/--queue-id`、`--provider-id/--provider`、`--cwd/--workdir`、`--model`、`--reasoning-effort`、`--execution-mode/--mode`、`--max-attempts` 和 `--reference-task-id/--reference/--ref`。这些字段写入任务 payload 后由 `code-queue-mgr` 入 PostgreSQL,核心任务字段包括 `queue_id`、`provider_id`、`execution_mode`、`model`、`cwd`、`prompt/base_prompt`、`reference_task_ids`、`reasoning_effort`、`max_attempts` 和 `task_json`;队列记录至少有 `id/name/created_at/updated_at`。模型治理应优先看任务 payload 和数据库字段,不靠 worker final response 自报。 -运行态默认模型仍是 `gpt-5.5`。`CODE_QUEUE_MODELS` 当前长期合同至少包含 GPT-5.5、GPT-5.4、GPT-5.4 Mini 和 MiniMax M2.7;`minimax-m2.7` 会走 OpenCode port,其余模型走 Codex port。DeepSeek 属于 dry-run policy 和指挥治理层推荐模型;只有当执行面 `/health` 或等价配置已经显示 DeepSeek 模型可用、并完成轻量 runner smoke 后,才允许真实提交 `--model deepseek-chat`。补 DeepSeek 运行态配置必须作为单独任务处理,不能在派单治理任务里顺手改 admission 或 Kubernetes manifest。 +运行态默认模型仍是 `gpt-5.5`。`CODE_QUEUE_MODELS` 当前长期合同至少包含 GPT-5.5、GPT-5.4、GPT-5.4 Mini、DeepSeek Chat 和 MiniMax M2.7;`deepseek`/`deepseek-chat` 与 `minimax-m2.7` 会走 OpenCode port,其余模型走 Codex port。只有当执行面 `/health` 或等价配置已经显示 DeepSeek 模型可用、并完成轻量 runner smoke 后,才允许真实提交 `--model deepseek-chat`。 -`codex submit --dry-run` 是派单前的轻量 preflight。它只输出 `routingRecommendation` 和 `policyContract`,帮助指挥官看到推荐 runner/model、风险信号、缺失的 prompt guard、模型分层和并发上限;它不会修改真实提交 payload,也不会替代指挥官判断。真实派单是否使用 `--model minimax-m2.7`、`--model deepseek-chat` 或 `--model gpt-5.5` 仍由指挥官显式决定。 +`codex submit --dry-run` 是派单前的轻量 preflight。它输出 `routingRecommendation`、`policyContract` 和模型注册表,帮助指挥官看到推荐 runner/model、风险信号、缺失的 prompt guard、模型分层、并发上限、`opencodeModels` 和 `modelPorts`;它不会修改真实提交 payload,也不会替代指挥官判断。真实派单是否使用 `--model minimax-m2.7`、`--model deepseek-chat` 或 `--model gpt-5.5` 仍由指挥官显式决定。 并发治理按模型和风险一起决定。GPT-5.5 常规并发目标是 5 条 lane;当写入范围互不重叠、heartbeat/trace 健康、完成质量稳定时可以短时提高到 10。MiniMax 只承接简单任务时可以提高到 10,但必须保留指挥官审阅和证据核验。DeepSeek 用于中等复杂度任务,默认按约 5 条 lane 观察质量,再根据成功率和 reviewer 负载逐步调整。并发扩张的前提永远是任务质量和可观测性,而不是模型价格。 @@ -58,7 +58,7 @@ Code Queue 派单模型按成本、可信度和 blast radius 分层:GPT-5.5/Co | 首选模型 | 适用任务 | 必须满足 | 禁止下放 | | --- | --- | --- | --- | | GPT-5.5/Codex | 高风险、复杂、跨模块、运行态、CI/CD、release、deploy、安全、最终质量裁决 | 多信号诊断、可回滚边界、必要的轻量或 dev 验证 | 不因成本把运行态和生产风险降级 | -| DeepSeek/Codex | 中等复杂度的前端功能、局部用户服务模块、局部 CLI/helper、明确 contract guard 或 unit test | prompt 自包含、写入范围窄、无生产/密钥/DB 写入、验证命令明确、指挥官审阅 | 不处理 Code Queue runtime、backend-core、provider-gateway、k3sctl-adapter、release/v1 或部署变更 | +| DeepSeek/OpenCode | 中等复杂度的前端功能、局部用户服务模块、局部 CLI/helper、明确 contract guard 或 unit test | prompt 自包含、写入范围窄、无生产/密钥/DB 写入、验证命令明确、指挥官审阅 | 不处理 Code Queue runtime、backend-core、provider-gateway、k3sctl-adapter、release/v1 或部署变更 | | MiniMax/OpenCode | 只读调查、文档、简单前端/样式、低风险样板、轻量 dry-run/preflight 和小范围测试补齐 | issue 只作辅助引用、必须给出 diff/路径/命令证据、完成后保持未读待审 | 不处理共享核心、隐式远端状态、生产、密钥、DB、重启、复杂 bug 或最终裁决 | | 指挥官/人工 | 真实生产动作、运行中任务控制、密钥/数据库/破坏性 Git、批量已读和高风险恢复 | 用户授权、只读诊断、恢复方案、记录 issue/#20/#24 | 不把执行权交给普通 worker | diff --git a/scripts/code-queue-submit-routing-contract-test.ts b/scripts/code-queue-submit-routing-contract-test.ts index e954c1ab..947ee371 100644 --- a/scripts/code-queue-submit-routing-contract-test.ts +++ b/scripts/code-queue-submit-routing-contract-test.ts @@ -1,4 +1,4 @@ -import { codexSubmitRoutingRecommendationForTest } from "./src/code-queue"; +import { codexSubmitModelRegistryForTest, codexSubmitRoutingRecommendationForTest } from "./src/code-queue"; type JsonRecord = Record; @@ -53,16 +53,30 @@ export function runCodeQueueSubmitRoutingContract(): JsonRecord { assertCondition(runtime.recommendedModel === "gpt-5.5", "runtime/core work should recommend GPT-5.5", runtime); const medium = codexSubmitRoutingRecommendationForTest(mediumPrompt, "deepseek"); - assertCondition(medium.route === "deepseek-codex", "medium bounded frontend work should recommend DeepSeek", medium); - assertCondition(medium.recommendedRunner === "codex", "DeepSeek work should use Codex runner", medium); + assertCondition(medium.route === "deepseek-opencode", "medium bounded frontend work should recommend DeepSeek/OpenCode", medium); + assertCondition(medium.recommendedRunner === "opencode", "DeepSeek work should use OpenCode runner", medium); assertCondition(medium.recommendedModel === "deepseek-chat", "DeepSeek candidate should recommend deepseek-chat", medium); assertCondition(asRecord(medium.riskControls).mediumComplexityCandidate === true, "medium prompt should satisfy medium complexity controls", medium); assertCondition(asRecord(medium.explicitRequest).model === "deepseek-chat", "explicit deepseek alias should normalize to deepseek-chat", medium); + assertCondition(asRecord(medium.explicitRequest).runner === "opencode", "explicit deepseek alias should route to OpenCode", medium); const policyContract = asRecord(medium.policyContract); assertCondition(asRecord(policyContract.concurrency).gpt55Routine === 5, "policy contract should expose GPT-5.5 routine concurrency", policyContract); assertCondition(asRecord(policyContract.concurrency).gpt55BurstMax === 10, "policy contract should expose GPT-5.5 burst concurrency", policyContract); assertCondition(asRecord(policyContract.concurrency).minimaxSimpleMax === 10, "policy contract should expose MiniMax simple concurrency", policyContract); assertCondition(asRecord(policyContract.concurrency).deepseekMediumDefault === 5, "policy contract should expose DeepSeek medium default concurrency", policyContract); + const modelTiers = asRecord(policyContract).modelTiers as unknown[]; + assertCondition(Array.isArray(modelTiers), "policy contract should expose model tiers", policyContract); + const deepseekTier = modelTiers.map(asRecord).find((tier) => tier.model === "deepseek-chat"); + assertCondition(deepseekTier?.runner === "opencode", "DeepSeek policy tier should use OpenCode", policyContract); + + const registry = codexSubmitModelRegistryForTest(["gpt-5.5", "deepseek", "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["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.codexModels.includes("gpt-5.5"), "codexModels should include default GPT", registry); const commanderOnly = codexSubmitRoutingRecommendationForTest(commanderOnlyPrompt); assertCondition(commanderOnly.route === "commander-human-only", "prod restart/secrets/DB work should be commander-only", commanderOnly); @@ -80,7 +94,8 @@ export function runCodeQueueSubmitRoutingContract(): JsonRecord { checks: [ "low-risk self-contained prompts recommend minimax-m2.7/OpenCode", "runtime/core work recommends GPT-5.5/Codex", - "medium bounded frontend work recommends deepseek-chat/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", "dry-run policy contract exposes model-tier concurrency", "prod/restart/secret/DB work is commander-only", "explicit --model mismatch is visible and payload is unchanged", diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index 45030d4f..e167e8c4 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -3,6 +3,7 @@ import { runCommand } from "./command"; import { type UniDeskConfig, repoRoot, rootPath } from "./config"; import { coreInternalFetch } from "./microservices"; import { previewJson } from "./preview"; +import { codeAgentPortForModel, codeModelPorts as sharedCodeModelPorts, defaultCodeModels as sharedDefaultCodeModels, opencodeModels as sharedOpencodeModels } from "../../src/components/microservices/code-queue/src/code-agent/common"; const defaultToolLimit = 8; const defaultTraceLimit = 80; @@ -60,7 +61,7 @@ interface CodexSubmitOptions { dryRun: boolean; } -type SubmitRoute = "minimax-opencode" | "deepseek-codex" | "gpt-5.5-codex" | "commander-human-only"; +type SubmitRoute = "minimax-opencode" | "deepseek-opencode" | "gpt-5.5-codex" | "commander-human-only"; type SubmitRouteSignalSeverity = "info" | "warning" | "block"; interface SubmitRouteSignal { @@ -626,10 +627,25 @@ function normalizeSubmitModel(value: string | null | undefined): string { return raw; } +function submitModelRegistry(models: string[] = sharedDefaultCodeModels): { + codeModels: string[]; + codexModels: string[]; + opencodeModels: string[]; + modelPorts: Record; +} { + const codeModels = Array.from(new Set(models.map((model) => normalizeSubmitModel(model)).filter(Boolean))); + return { + codeModels, + codexModels: codeModels.filter((model) => codeAgentPortForModel(model) === "codex"), + opencodeModels: sharedOpencodeModels(codeModels), + modelPorts: sharedCodeModelPorts(codeModels), + }; +} + function submitRunnerForModel(model: string | null | undefined): "opencode" | "codex" | null { const normalized = normalizeSubmitModel(model); if (normalized.length === 0) return null; - return normalized === minimaxSubmitModel ? "opencode" : "codex"; + return normalized === minimaxSubmitModel || normalized === deepseekSubmitModel ? "opencode" : "codex"; } function regexEvidence(text: string, patterns: RegExp[], limit = 6): string[] { @@ -667,7 +683,7 @@ function submitPolicyContract(): SubmitRoutingRecommendation["policyContract"] { return { selectionPrinciples: [ "Use GPT-5.5 for high-risk, runtime/core, security, CI/CD, deploy, release, and final quality calls.", - "Use DeepSeek for self-contained medium-complexity work with limited write scope and verifiable tests.", + "Use DeepSeek/OpenCode for self-contained medium-complexity work with limited write scope and verifiable tests.", "Use MiniMax only for simple, low-risk, self-contained work with external evidence and commander review.", "Keep prod restart, secret access, DB writes, destructive Git, and running-task control with the commander or human.", ], @@ -686,7 +702,7 @@ function submitPolicyContract(): SubmitRoutingRecommendation["policyContract"] { }, { model: deepseekSubmitModel, - runner: "codex", + runner: "opencode", taskRisk: "medium-complexity", requiredGuards: ["self-contained prompt", "limited write scope", "contract/unit verification", "commander review"], }, @@ -844,17 +860,17 @@ function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRouting confidence = "high"; reason = "This task touches runtime/core/cross-module or release-governance surfaces, so it should stay on GPT-5.5."; } else if (mediumComplexityCandidate) { - route = "deepseek-codex"; - recommendedRunner = "codex"; + route = "deepseek-opencode"; + recommendedRunner = "opencode"; recommendedModel = deepseekSubmitModel; confidence = "high"; - reason = "The prompt looks self-contained, medium-complexity, and verifiable without production/state privileges; it is a DeepSeek/Codex candidate after commander review."; + reason = "The prompt looks self-contained, medium-complexity, and verifiable without production/state privileges; it is a DeepSeek/OpenCode candidate after commander review."; } else if (mediumComplexityEvidence.length > 0 && issueIsNotOnlySource && noProdRestartSecretOrDbWrite && noRuntimeCoreOrReleaseWork) { - route = "deepseek-codex"; - recommendedRunner = "codex"; + route = "deepseek-opencode"; + recommendedRunner = "opencode"; recommendedModel = deepseekSubmitModel; confidence = "medium"; - reason = "The prompt has medium-complexity signals, but the commander should tighten self-contained context, write scope, and verification requirements before relying on DeepSeek."; + reason = "The prompt has medium-complexity signals, but the commander should tighten self-contained context, write scope, and verification requirements before relying on DeepSeek/OpenCode."; } else if (promptSelfContained && issueIsNotOnlySource && evidenceRequiredByPrompt && lowRiskEvidence.length > 0) { route = "minimax-opencode"; recommendedRunner = "opencode"; @@ -2829,6 +2845,10 @@ export function codexSubmitRoutingRecommendationForTest(prompt: string, model?: }); } +export function codexSubmitModelRegistryForTest(models: string[] = sharedDefaultCodeModels): ReturnType { + return submitModelRegistry(models); +} + function codexSubmitTask(args: string[]): unknown { const options = parseSubmitOptions(args); const payload = submitPayload(options); @@ -2837,6 +2857,7 @@ function codexSubmitTask(args: string[]): unknown { ok: true, dryRun: true, routingRecommendation: submitRoutingRecommendation(options), + modelRegistry: submitModelRegistry(), request: { ...payload, prompt: textView(options.prompt, true, 3000), diff --git a/src/components/frontend/src/code-queue.tsx b/src/components/frontend/src/code-queue.tsx index f6719497..d1669eae 100644 --- a/src/components/frontend/src/code-queue.tsx +++ b/src/components/frontend/src/code-queue.tsx @@ -1642,7 +1642,7 @@ function transcriptResumeSeq(transcript: any[], overlapRows = 8): number { function codexModelOptions(queue: any, currentModel: string): string[] { const configured = Array.isArray(queue?.codeModels) ? queue.codeModels : Array.isArray(queue?.codexModels) ? queue.codexModels : []; - const fallback = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", "minimax-m2.7"]; + const fallback = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", "deepseek-chat", "minimax-m2.7"]; return Array.from(new Set([...configured, ...fallback, currentModel].map((item) => String(item || "").trim()).filter(Boolean))); } diff --git a/src/components/microservices/code-queue-mgr/src-rs/main.rs b/src/components/microservices/code-queue-mgr/src-rs/main.rs index 897ad85a..0e285712 100644 --- a/src/components/microservices/code-queue-mgr/src-rs/main.rs +++ b/src/components/microservices/code-queue-mgr/src-rs/main.rs @@ -21,6 +21,8 @@ const MAX_PREVIEW_CHARS: usize = 6000; const WORKDIR_MAX_CHARS: usize = 512; const SCHEDULER_HEARTBEAT_STALE_MS: i64 = 5 * 60 * 1000; const CODEX_STATS_TIME_ZONE: &str = "Asia/Shanghai"; +const DEEPSEEK_CHAT_MODEL: &str = "deepseek-chat"; +const MINIMAX_M27_MODEL: &str = "minimax-m2.7"; #[derive(Clone)] struct Config { @@ -130,13 +132,17 @@ fn normalize_code_model(value: &str) -> String { let lower = raw.to_ascii_lowercase(); let leaf = lower.rsplit('/').next().unwrap_or(&lower); if leaf == "m2.7" || leaf == "minimax-m2.7" { - return "minimax-m2.7".to_string(); + return MINIMAX_M27_MODEL.to_string(); + } + if leaf == "deepseek" || leaf == "deepseek-chat" { + return DEEPSEEK_CHAT_MODEL.to_string(); } raw.to_string() } fn code_agent_port(model: &str) -> &'static str { - if normalize_code_model(model) == "minimax-m2.7" { + let normalized = normalize_code_model(model); + if normalized == MINIMAX_M27_MODEL || normalized == DEEPSEEK_CHAT_MODEL { "opencode" } else { "codex" @@ -208,7 +214,7 @@ fn config_from_env() -> Result { return Err("DATABASE_URL is required".to_string()); } let default_model = normalize_code_model(&env_string("CODE_QUEUE_DEFAULT_MODEL", "gpt-5.5")); - let model_source = env_string("CODE_QUEUE_MODELS", "gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7"); + let model_source = env_string("CODE_QUEUE_MODELS", "gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7"); let mut code_models = vec![default_model.clone()]; for item in model_source.split(',') { let model = normalize_code_model(item); diff --git a/src/components/microservices/code-queue-mgr/src/index.ts b/src/components/microservices/code-queue-mgr/src/index.ts index b4a3887e..dfc6e2bd 100644 --- a/src/components/microservices/code-queue-mgr/src/index.ts +++ b/src/components/microservices/code-queue-mgr/src/index.ts @@ -304,7 +304,9 @@ const codeQueueEnvironmentHint = [ ].join("\n"); const maxTaskAttempts = 99; const workdirMaxLength = 512; -const defaultCodeModels = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", "minimax-m2.7"]; +const deepseekChatModel = "deepseek-chat"; +const minimaxM27Model = "minimax-m2.7"; +const defaultCodeModels = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", deepseekChatModel, minimaxM27Model]; const codeExecutionModes: CodeExecutionMode[] = ["default", "windows-native"]; const codexStatsTimeZone = "Asia/Shanghai"; const schedulerHeartbeatStaleMs = 5 * 60 * 1000; @@ -503,12 +505,14 @@ function normalizeCodeModel(value: string): string { if (raw.length === 0) return raw; const lower = raw.toLowerCase(); const leaf = lower.includes("/") ? lower.split("/").at(-1) ?? lower : lower; - if (leaf === "minimax-m2.7" || leaf === "m2.7") return "minimax-m2.7"; + if (leaf === "minimax-m2.7" || leaf === "m2.7") return minimaxM27Model; + if (leaf === "deepseek" || leaf === "deepseek-chat") return deepseekChatModel; return raw; } function codeAgentPortForModel(model: string): "codex" | "opencode" { - return normalizeCodeModel(model) === "minimax-m2.7" ? "opencode" : "codex"; + const normalized = normalizeCodeModel(model); + return normalized === minimaxM27Model || normalized === deepseekChatModel ? "opencode" : "codex"; } function codeAgentPortInfo(kind: "codex" | "opencode"): JsonRecord { diff --git a/src/components/microservices/code-queue/docker-compose.d601.yml b/src/components/microservices/code-queue/docker-compose.d601.yml index 21c31274..dd29c94a 100644 --- a/src/components/microservices/code-queue/docker-compose.d601.yml +++ b/src/components/microservices/code-queue/docker-compose.d601.yml @@ -28,7 +28,7 @@ services: CODE_QUEUE_OPENCODE_XDG_DIR: "/var/lib/unidesk/code-queue/opencode-xdg" CODE_QUEUE_SOURCE_CODEX_CONFIG: "/root/.codex/config.toml" CODE_QUEUE_DEFAULT_MODEL: "${CODE_QUEUE_DEFAULT_MODEL:-gpt-5.5}" - CODE_QUEUE_MODELS: "${CODE_QUEUE_MODELS:-gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7}" + CODE_QUEUE_MODELS: "${CODE_QUEUE_MODELS:-gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7}" CODE_QUEUE_MODEL_REASONING_EFFORTS: "${CODE_QUEUE_MODEL_REASONING_EFFORTS:-gpt-5.5=xhigh}" CODE_QUEUE_SANDBOX: "${CODE_QUEUE_SANDBOX:-danger-full-access}" CODE_QUEUE_APPROVAL_POLICY: "${CODE_QUEUE_APPROVAL_POLICY:-never}" diff --git a/src/components/microservices/code-queue/src/code-agent/common.ts b/src/components/microservices/code-queue/src/code-agent/common.ts index 1e3001bf..c7e7f9d2 100644 --- a/src/components/microservices/code-queue/src/code-agent/common.ts +++ b/src/components/microservices/code-queue/src/code-agent/common.ts @@ -33,7 +33,8 @@ export interface ActiveRunSlotWaiter { const gitProxyEnvKeys = ["CODE_QUEUE_EGRESS_PROXY_URL", "HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"]; export const minimaxM27Model = "minimax-m2.7"; -export const defaultCodeModels = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", minimaxM27Model]; +export const deepseekChatModel = "deepseek-chat"; +export const defaultCodeModels = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", deepseekChatModel, minimaxM27Model]; export const opencodeNpmPackage = "opencode-ai@1.14.48"; export const defaultCodeExecutionMode: CodeExecutionMode = "default"; export const codeExecutionModes: CodeExecutionMode[] = ["default", "windows-native"]; @@ -77,11 +78,13 @@ export function normalizeCodeModel(value: string): string { const lower = raw.toLowerCase(); const leaf = lower.includes("/") ? lower.split("/").at(-1) ?? lower : lower; if (leaf === "minimax-m2.7" || leaf === "m2.7") return minimaxM27Model; + if (leaf === "deepseek" || leaf === "deepseek-chat") return deepseekChatModel; return raw; } export function codeAgentPortForModel(model: string): CodeAgentPortKind { - return normalizeCodeModel(model) === minimaxM27Model ? "opencode" : "codex"; + const normalized = normalizeCodeModel(model); + return normalized === minimaxM27Model || normalized === deepseekChatModel ? "opencode" : "codex"; } export function normalizeCodeExecutionMode(value: unknown): CodeExecutionMode { @@ -112,7 +115,7 @@ export function codeExecutionModeInfo(mode: CodeExecutionMode): Record codeAgentPortForModel(model) === "opencode"); + return Array.from(new Set(models.filter((model) => codeAgentPortForModel(model) === "opencode"))); } export function codeModelPorts(models: string[]): Record { diff --git a/src/components/microservices/code-queue/src/code-agent/opencode.ts b/src/components/microservices/code-queue/src/code-agent/opencode.ts index 63a92f42..49f9e7c1 100644 --- a/src/components/microservices/code-queue/src/code-agent/opencode.ts +++ b/src/components/microservices/code-queue/src/code-agent/opencode.ts @@ -6,11 +6,11 @@ import { resolve } from "node:path"; import * as readline from "node:readline"; import type { AppServerExit, CodexEventSummary, CodexRunResult, JsonValue, QueueTask, RuntimeConfig, TerminalStatus } from "../types"; import type { ActiveRun, CodeAgentClient } from "./common"; -import { codeAgentGitConfigEntries, extractRecord, minimaxM27Model, normalizeCodeModel, stripAnsi, withCodeAgentGitConfigEnv } from "./common"; +import { codeAgentGitConfigEntries, deepseekChatModel, extractRecord, minimaxM27Model, normalizeCodeModel, stripAnsi, withCodeAgentGitConfigEnv } from "./common"; import { classifyRunnerError, runnerErrorClassificationJson } from "../runner-error-classifier"; export interface OpenCodePortContext { - config: Pick; + config: Pick; activeRuns: Map; addEvent: (task: QueueTask, event: CodexEventSummary) => void; appendOutput: (task: QueueTask, channel: "system" | "assistant" | "reasoning" | "command" | "diff" | "tool" | "error", text: string, method?: string, itemId?: string, append?: boolean) => unknown; @@ -69,13 +69,21 @@ function splitOpenCodeAssistantText(text: string): OpenCodeTextParts { } function openCodeModelId(model: string): string { - if (normalizeCodeModel(model) !== minimaxM27Model) throw new Error(`OpenCode port does not support model ${model}`); - const providerModel = ctx().config.minimaxModel.trim() || "MiniMax-M2.7"; - return `minimax/${providerModel}`; + const normalized = normalizeCodeModel(model); + if (normalized === minimaxM27Model) { + const providerModel = ctx().config.minimaxModel.trim() || "MiniMax-M2.7"; + return `minimax/${providerModel}`; + } + if (normalized === deepseekChatModel) { + const providerModel = ctx().config.deepseekModel.trim() || "deepseek-chat"; + return `deepseek/${providerModel}`; + } + throw new Error(`OpenCode port does not support model ${model}`); } function openCodeConfigContent(): string { const providerModel = ctx().config.minimaxModel.trim() || "MiniMax-M2.7"; + const deepseekModel = ctx().config.deepseekModel.trim() || "deepseek-chat"; return JSON.stringify({ $schema: "https://opencode.ai/config.json", provider: { @@ -93,6 +101,20 @@ function openCodeConfigContent(): string { }, }, }, + deepseek: { + npm: "@ai-sdk/openai-compatible", + name: "DeepSeek", + options: { + baseURL: ctx().config.deepseekApiBase, + apiKey: "{env:DEEPSEEK_API_KEY}", + }, + models: { + [deepseekModel]: { + name: deepseekChatModel, + limit: { context: 128000, output: 16384 }, + }, + }, + }, }, }); } @@ -111,6 +133,9 @@ function openCodeEnv(task: QueueTask): NodeJS.ProcessEnv { return withCodeAgentGitConfigEnv({ ...process.env, ...xdgEnv, + DEEPSEEK_API_KEY: ctx().config.deepseekApiKey, + DEEPSEEK_API_BASE: ctx().config.deepseekApiBase, + DEEPSEEK_MODEL: ctx().config.deepseekModel, MINIMAX_API_KEY: ctx().config.minimaxApiKey, MINIMAX_API_BASE: ctx().config.minimaxApiBase, MINIMAX_MODEL: ctx().config.minimaxModel, @@ -135,6 +160,8 @@ function remoteOpenCodeRunCommand(task: QueueTask, prompt: string): string { const gitConfigEntries = codeAgentGitConfigEntries(""); const envExports = [ ...Object.entries(xdgEnv).map(([key, value]) => `export ${key}=${ctx().shellQuote(value)}`), + `export DEEPSEEK_API_BASE=${ctx().shellQuote(ctx().config.deepseekApiBase)}`, + `export DEEPSEEK_MODEL=${ctx().shellQuote(ctx().config.deepseekModel)}`, `export MINIMAX_API_BASE=${ctx().shellQuote(ctx().config.minimaxApiBase)}`, `export MINIMAX_MODEL=${ctx().shellQuote(ctx().config.minimaxModel)}`, `export OPENCODE_CONFIG_CONTENT=${ctx().shellQuote(openCodeConfigContent())}`, @@ -351,6 +378,13 @@ function classifyOpenCodeRunnerFailure(task: QueueTask, result: Pick { const attemptedSessionId = task.codexThreadId; const first = await runOpenCodeTurnOnce(task, prompt); @@ -366,8 +400,9 @@ export async function runOpenCodeTurn(task: QueueTask, prompt: string): Promise< async function runOpenCodeTurnOnce(task: QueueTask, prompt: string): Promise { const queueId = ctx().queueIdOf(task); - if (ctx().config.minimaxApiKey.length === 0) { - const message = "MINIMAX_API_KEY is required for opencode model minimax-m2.7."; + const missingCredential = missingOpenCodeCredentialMessage(task.model); + if (missingCredential !== null) { + const message = missingCredential; ctx().appendOutput(task, "error", `${message}\n`, "opencode/config"); return { threadId: task.codexThreadId, diff --git a/src/components/microservices/code-queue/src/index.ts b/src/components/microservices/code-queue/src/index.ts index 12c53253..bd4fb06d 100644 --- a/src/components/microservices/code-queue/src/index.ts +++ b/src/components/microservices/code-queue/src/index.ts @@ -385,7 +385,7 @@ function readConfig(): RuntimeConfig { mainProviderId, remoteDefaultWorkdir, executionProviderIds, - remoteCodexEnvKeys: envList("CODE_QUEUE_REMOTE_CODEX_ENV_KEYS", ["OPENAI_API_KEY", "CRS_OAI_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "MINIMAX_API_KEY", "MINIMAX_API_BASE", "MINIMAX_MODEL", "GH_TOKEN", "GITHUB_TOKEN", "GH_HOST", "GITHUB_API_URL", "GH_REPO"]), + remoteCodexEnvKeys: envList("CODE_QUEUE_REMOTE_CODEX_ENV_KEYS", ["OPENAI_API_KEY", "CRS_OAI_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "DEEPSEEK_API_KEY", "DEEPSEEK_API_BASE", "DEEPSEEK_MODEL", "MINIMAX_API_KEY", "MINIMAX_API_BASE", "MINIMAX_MODEL", "GH_TOKEN", "GITHUB_TOKEN", "GH_HOST", "GITHUB_API_URL", "GH_REPO"]), skillsPath: envString("UNIDESK_SKILLS_PATH", "/root/.agents/skills"), codexHome: envString("CODE_QUEUE_CODEX_HOME", "/var/lib/unidesk/code-queue/codex-home"), opencodeXdgDir: envString("CODE_QUEUE_OPENCODE_XDG_DIR", resolve(dataDir, "opencode-xdg")), @@ -397,13 +397,16 @@ function readConfig(): RuntimeConfig { memoryWatchdogThresholdBytes: Math.max(0, envNumber("CODE_QUEUE_MEMORY_WATCHDOG_THRESHOLD_BYTES", 0)), memoryWatchdogIntervalMs: Math.max(1000, Math.min(60_000, envNumber("CODE_QUEUE_MEMORY_WATCHDOG_INTERVAL_MS", 2000))), defaultModel, - codexModels: codeModels, + codexModels: codeModels.filter((model) => codeAgentPortForModel(model) === "codex"), defaultReasoningEffort: envNullableString("CODE_QUEUE_REASONING_EFFORT"), modelReasoningEfforts: envModelReasoningEfforts("CODE_QUEUE_MODEL_REASONING_EFFORTS", { "gpt-5.5": "xhigh" }), sandbox: sandboxValue(envString("CODE_QUEUE_SANDBOX", "danger-full-access")), approvalPolicy: approvalValue(envString("CODE_QUEUE_APPROVAL_POLICY", "never")), defaultMaxAttempts: clampTaskAttempts(envNumber("CODE_QUEUE_MAX_ATTEMPTS", maxTaskAttempts)), codeModels, + deepseekApiKey: envString("DEEPSEEK_API_KEY", ""), + deepseekApiBase: envString("DEEPSEEK_API_BASE", "https://api.deepseek.com").replace(/\/+$/u, ""), + deepseekModel: envString("DEEPSEEK_MODEL", "deepseek-chat"), minimaxApiKey: envString("MINIMAX_API_KEY", ""), minimaxApiBase: envString("MINIMAX_API_BASE", "https://api.minimaxi.com/v1").replace(/\/+$/u, ""), minimaxModel: envString("MINIMAX_MODEL", "MiniMax-M2.7"), diff --git a/src/components/microservices/code-queue/src/types.ts b/src/components/microservices/code-queue/src/types.ts index b7df7468..7bb72877 100644 --- a/src/components/microservices/code-queue/src/types.ts +++ b/src/components/microservices/code-queue/src/types.ts @@ -141,6 +141,9 @@ export interface RuntimeConfig { approvalPolicy: "untrusted" | "on-failure" | "on-request" | "never"; defaultMaxAttempts: number; codeModels: string[]; + deepseekApiKey: string; + deepseekApiBase: string; + deepseekModel: string; minimaxApiKey: string; minimaxApiBase: string; minimaxModel: string; diff --git a/src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml b/src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml index 0dc03cd9..f65ef7e2 100644 --- a/src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml +++ b/src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml @@ -964,7 +964,7 @@ spec: - name: CODE_QUEUE_DEFAULT_MODEL value: "gpt-5.5" - name: CODE_QUEUE_MODELS - value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7" + value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7" - name: CODE_QUEUE_DATABASE_POOL_MAX value: "4" - name: CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS diff --git a/src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml b/src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml index 4ebead80..d031aeb1 100644 --- a/src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml +++ b/src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml @@ -83,7 +83,7 @@ spec: - name: CODE_QUEUE_DEFAULT_MODEL value: "gpt-5.5" - name: CODE_QUEUE_MODELS - value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7" + value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7" - name: CODE_QUEUE_MODEL_REASONING_EFFORTS value: "gpt-5.5=xhigh" - name: CODE_QUEUE_SANDBOX @@ -320,7 +320,7 @@ spec: - name: CODE_QUEUE_DEFAULT_MODEL value: "gpt-5.5" - name: CODE_QUEUE_MODELS - value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7" + value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7" - name: CODE_QUEUE_MODEL_REASONING_EFFORTS value: "gpt-5.5=xhigh" - name: CODE_QUEUE_SANDBOX @@ -1016,7 +1016,7 @@ spec: - name: CODE_QUEUE_DEFAULT_MODEL value: "gpt-5.5" - name: CODE_QUEUE_MODELS - value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7" + value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7" - name: CODE_QUEUE_MODEL_REASONING_EFFORTS value: "gpt-5.5=xhigh" - name: CODE_QUEUE_SANDBOX diff --git a/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml b/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml index 2c2f3e4d..a4a397b1 100644 --- a/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml +++ b/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml @@ -143,7 +143,7 @@ data: CODE_QUEUE_CODEX_HOME: /var/lib/unidesk-dev/code-queue/codex-home CODE_QUEUE_OPENCODE_XDG_DIR: /var/lib/unidesk-dev/code-queue/opencode-xdg CODE_QUEUE_DEFAULT_MODEL: gpt-5.5 - CODE_QUEUE_MODELS: gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7 + CODE_QUEUE_MODELS: gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7 CODE_QUEUE_MODEL_REASONING_EFFORTS: gpt-5.5=xhigh CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: "false" CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED: "false"