feat(agent): unify models and validate OpenCode providers

Generate chat, task and learning models from agent-models.json.
Use WaterNetwork fast/expert models with 272k context and validate
upstream names before readiness. Add an isolated model check command
with signal-safe cleanup and plugin, Skill and MCP management docs.

Verified in isolation: type checks, 26 focused tests and both model
catalog checks; no model inference requests.
This commit is contained in:
2026-10-09 11:28:37 +08:00
parent 75d8d526f9
commit d58bd19039
30 changed files with 1125 additions and 469 deletions
+17 -46
View File
@@ -1,11 +1,7 @@
import dotenv from "dotenv";
import { z } from "zod";
import {
defaultAgentModelOptionsJson,
getAgentModelIds,
parseAgentModelOptions,
} from "./chat/modelConfig.js";
import { loadAgentModelConfig, type AgentModelConfig } from "./chat/modelConfig.js";
// 本地开发可在项目根目录放 .local.env;已存在的系统环境变量优先级更高。
dotenv.config({ path: ".local.env", override: false });
@@ -73,12 +69,8 @@ const envSchema = z
OPENCODE_PORT: z.coerce.number().int().nonnegative().default(0),
// OpenCode 2 启动超时时间(毫秒)。
OPENCODE_TIMEOUT_MS: z.coerce.number().int().positive().default(5000),
// 默认使用的 opencode 模型标识。
OPENCODE_MODEL: z.string().default("deepseek/deepseek-flash"),
// 后台工况任务首选模型;缺省沿用 OPENCODE_MODEL,运行时会从实际可用模型中选择回退。
OPERATIONAL_TASK_MODEL: optionalString(),
// 聊天 UI 和 /stream 允许选择的 opencode 模型完整配置,JSON 数组。
OPENCODE_MODEL_OPTIONS: z.string().default(defaultAgentModelOptionsJson),
// 可选的统一模型配置文件;未设置时读取仓库中的 agent-models.json。
AGENT_MODEL_CONFIG_PATH: optionalString(),
// 平滑 token 输出,避免上游模型大块/抖动 token 直接传到前端。
AGENT_TOKEN_SMOOTHING_ENABLED: optionalBoolean(true),
// 平滑输出每段之间的间隔(毫秒)。
@@ -161,42 +153,14 @@ const envSchema = z
message: "AGENT_INTERNAL_TOKEN is required in client mode and must also be configured on the external OpenCode process",
});
}
let modelOptions;
try {
modelOptions = parseAgentModelOptions(env.OPENCODE_MODEL_OPTIONS);
} catch (error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["OPENCODE_MODEL_OPTIONS"],
message: error instanceof Error ? error.message : String(error),
});
return;
}
const supportedModels = getAgentModelIds(modelOptions);
if (supportedModels.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["OPENCODE_MODEL_OPTIONS"],
message: "OPENCODE_MODEL_OPTIONS must include at least one model",
});
}
if (!supportedModels.includes(env.OPENCODE_MODEL)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["OPENCODE_MODEL"],
message: "OPENCODE_MODEL must be included in OPENCODE_MODEL_OPTIONS",
});
}
if (env.OPERATIONAL_TASK_MODEL && !supportedModels.includes(env.OPERATIONAL_TASK_MODEL)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["OPERATIONAL_TASK_MODEL"],
message: "OPERATIONAL_TASK_MODEL must be included in OPENCODE_MODEL_OPTIONS",
});
}
});
export type AppConfig = z.infer<typeof envSchema>;
export type AppConfig = z.infer<typeof envSchema> & {
MODEL_CONFIG: AgentModelConfig;
OPENCODE_MODEL: string;
OPERATIONAL_TASK_MODEL: string;
LEARNING_GATE_MODEL: string;
};
const normalizedEnv = {
...process.env,
@@ -209,4 +173,11 @@ const normalizedEnv = {
process.env.OPENCODE_CLIENT_BASE_URL ?? process.env.OPENCODE_BASE_URL,
};
export const config: AppConfig = envSchema.parse(normalizedEnv);
const environment = envSchema.parse(normalizedEnv);
const modelConfiguration = loadAgentModelConfig(environment.AGENT_MODEL_CONFIG_PATH);
export const config: AppConfig = {
...environment, MODEL_CONFIG: modelConfiguration,
OPENCODE_MODEL: modelConfiguration.default_model,
OPERATIONAL_TASK_MODEL: modelConfiguration.task_model ?? modelConfiguration.default_model,
LEARNING_GATE_MODEL: modelConfiguration.learning_gate_model ?? modelConfiguration.default_model,
};