fix: route deepseek through opencode

This commit is contained in:
Codex
2026-05-21 10:30:17 +00:00
parent 27d0ddff51
commit 8b8b74e953
14 changed files with 133 additions and 43 deletions
+1 -1
View File
@@ -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)));
}
@@ -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<Config, String> {
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);
@@ -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 {
@@ -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}"
@@ -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<string, J
}
export function opencodeModels(models: string[]): string[] {
return models.filter((model) => codeAgentPortForModel(model) === "opencode");
return Array.from(new Set(models.filter((model) => codeAgentPortForModel(model) === "opencode")));
}
export function codeModelPorts(models: string[]): Record<string, CodeAgentPortKind> {
@@ -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<RuntimeConfig, "defaultWorkdir" | "minimaxApiBase" | "minimaxApiKey" | "minimaxModel" | "turnNoActivityTimeoutMs">;
config: Pick<RuntimeConfig, "deepseekApiBase" | "deepseekApiKey" | "deepseekModel" | "defaultWorkdir" | "minimaxApiBase" | "minimaxApiKey" | "minimaxModel" | "turnNoActivityTimeoutMs">;
activeRuns: Map<string, ActiveRun>;
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<CodexRunRes
return runnerErrorClassificationJson(classifyRunnerError(message, task.providerId));
}
function missingOpenCodeCredentialMessage(model: string): string | null {
const normalized = normalizeCodeModel(model);
if (normalized === minimaxM27Model && ctx().config.minimaxApiKey.length === 0) return "MINIMAX_API_KEY is required for opencode model minimax-m2.7.";
if (normalized === deepseekChatModel && ctx().config.deepseekApiKey.length === 0) return "DEEPSEEK_API_KEY is required for opencode model deepseek-chat.";
return null;
}
export async function runOpenCodeTurn(task: QueueTask, prompt: string): Promise<CodexRunResult> {
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<CodexRunResult> {
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,
@@ -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"),
@@ -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;
@@ -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
@@ -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
@@ -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"