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
+13 -7
View File
@@ -8,6 +8,8 @@ import { realpath, rm } from "node:fs/promises";
import { createConversationWorkspace, resolveConversationWorkspace } from "./conversationWorkspace.js";
import { serviceOpencodeConfig } from "./opencode2-config.js";
import { mcpReadonlyTools, mcpRuntimeReportSchema } from "./mcp-config.js";
import { parseModelReference } from "../chat/modelConfig.js";
import { assertModelCheckPassed, modelCheckReportSchema, modelCheckTargets } from "./model-check.js";
import { config } from "../config.js";
import { logger } from "../logger.js";
@@ -127,6 +129,14 @@ export class OpencodeRuntimeAdapter {
return selected;
}
async checkModels(directory = process.cwd()) {
const client = await this.ensureClient();
const response = await client.rpc.call({
rpcID: "tjwater.tools", method: "check_models", input: modelCheckTargets(config.MODEL_CONFIG), location: { directory },
}, { signal: AbortSignal.timeout(30_000) });
return modelCheckReportSchema.parse(response.output);
}
async warmup(): Promise<void> {
const client = await this.ensureClient();
const health = await this.health();
@@ -148,6 +158,7 @@ export class OpencodeRuntimeAdapter {
if (!actualRoot || await realpath(actualRoot) !== await realpath(config.CONVERSATION_WORKSPACE_ROOT)) throw new Error("OpenCode and Agent conversation workspace roots do not match");
const mcp = await this.readMcpReport(client, session.location.directory);
if (mcp.configuration_errors.length) throw new Error("MCP configuration invalid");
assertModelCheckPassed(await this.checkModels(session.location.directory));
const models = await client.model.list({ location: { directory: session.location.directory } });
const model = models.data.find((model) => `${model.providerID}/${model.id}` === config.OPENCODE_MODEL);
if (!model?.enabled || !model.capabilities.tools) {
@@ -162,7 +173,7 @@ export class OpencodeRuntimeAdapter {
async createSession(title?: string) {
const client = await this.ensureClient();
const [providerID, id] = config.OPENCODE_MODEL.split("/");
const { providerID, modelID: id } = parseModelReference(config.OPENCODE_MODEL);
const directory = await createConversationWorkspace(config.CONVERSATION_WORKSPACE_ROOT);
try {
return await client.session.create({ title, agent: "instruction", model: { providerID, id }, location: { directory } });
@@ -468,12 +479,7 @@ export class OpencodeRuntimeAdapter {
export const opencodeRuntime = new OpencodeRuntimeAdapter();
function buildOpencodeConfig(): Record<string, unknown> {
return serviceOpencodeConfig(deepMerge(
deepMerge(readProjectOpencodeConfig(), readEnvOpencodeConfig()),
{
model: config.OPENCODE_MODEL,
},
));
return serviceOpencodeConfig(deepMerge(readProjectOpencodeConfig(), readEnvOpencodeConfig()), config.MODEL_CONFIG);
}
function readProjectOpencodeConfig(): Record<string, unknown> {