A/B test (2026-05-05) showed DeepSeek V4-Pro is 2-3x faster and ~20x cheaper than Sonnet for style/lexicon pattern analysis, with comparable quality. Adds adapters/deepseek-paperclip-adapter/ package, documents adapter requirements (env injection, run-id headers), updates CLAUDE.md with adapter integration notes, and records lessons from ערר 1200-25 (block order for 1xxx, "להלן מתוך" pattern, expanded factual background, bridge planning analysis, flat heading structure). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
165 lines
4.4 KiB
JavaScript
165 lines
4.4 KiB
JavaScript
/**
|
|
* Environment test for the DeepSeek (via Hermes) adapter.
|
|
*/
|
|
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import {
|
|
HERMES_CLI,
|
|
ADAPTER_TYPE,
|
|
DEFAULT_PROFILE_HOME,
|
|
} from "../shared/constants.js";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
function asString(v) {
|
|
return typeof v === "string" ? v : undefined;
|
|
}
|
|
|
|
async function checkCliInstalled(command) {
|
|
try {
|
|
await execFileAsync(command, ["--version"], { timeout: 10_000 });
|
|
return null;
|
|
} catch (err) {
|
|
if (err && err.code === "ENOENT") {
|
|
return {
|
|
level: "error",
|
|
message: `Hermes CLI "${command}" not found in PATH`,
|
|
hint: "Install Hermes Agent: pip install hermes-agent",
|
|
code: "deepseek_hermes_cli_not_found",
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function checkProfile(profileHome) {
|
|
try {
|
|
const stat = await fs.stat(profileHome);
|
|
if (!stat.isDirectory()) {
|
|
return {
|
|
level: "error",
|
|
message: `Profile path is not a directory: ${profileHome}`,
|
|
hint: "Create the directory or override hermesProfileHome in adapter config.",
|
|
code: "deepseek_profile_not_dir",
|
|
};
|
|
}
|
|
} catch {
|
|
return {
|
|
level: "error",
|
|
message: `Hermes profile dir does not exist: ${profileHome}`,
|
|
hint: "Create the profile dir with config.yaml + .env (DEEPSEEK_API_KEY).",
|
|
code: "deepseek_profile_missing",
|
|
};
|
|
}
|
|
|
|
const configPath = path.join(profileHome, "config.yaml");
|
|
try {
|
|
await fs.stat(configPath);
|
|
} catch {
|
|
return {
|
|
level: "error",
|
|
message: `Profile is missing config.yaml: ${configPath}`,
|
|
hint: "Add config.yaml with model.default + model.base_url + model.key_env.",
|
|
code: "deepseek_profile_no_config",
|
|
};
|
|
}
|
|
|
|
return {
|
|
level: "info",
|
|
message: `Profile resolved: ${profileHome}`,
|
|
code: "deepseek_profile_ok",
|
|
};
|
|
}
|
|
|
|
async function checkApiKey(profileHome, configEnv) {
|
|
// 1. config.env (resolved by Paperclip from secrets)
|
|
if (configEnv && typeof configEnv === "object" && asString(configEnv.DEEPSEEK_API_KEY)) {
|
|
return {
|
|
level: "info",
|
|
message: "DEEPSEEK_API_KEY found in adapter env config",
|
|
code: "deepseek_api_key_in_config",
|
|
};
|
|
}
|
|
// 2. Profile-local .env
|
|
try {
|
|
const envFile = path.join(profileHome, ".env");
|
|
const text = await fs.readFile(envFile, "utf-8");
|
|
if (/^\s*DEEPSEEK_API_KEY=/m.test(text)) {
|
|
return {
|
|
level: "info",
|
|
message: `DEEPSEEK_API_KEY found in ${envFile}`,
|
|
code: "deepseek_api_key_in_profile",
|
|
};
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
// 3. Process env
|
|
if (process.env.DEEPSEEK_API_KEY) {
|
|
return {
|
|
level: "info",
|
|
message: "DEEPSEEK_API_KEY found in Paperclip process env",
|
|
code: "deepseek_api_key_in_process",
|
|
};
|
|
}
|
|
return {
|
|
level: "error",
|
|
message: "DEEPSEEK_API_KEY not found in adapter env, profile .env, or process env",
|
|
hint: "Add DEEPSEEK_API_KEY to <HERMES_HOME>/.env or to the agent's env secrets.",
|
|
code: "deepseek_api_key_missing",
|
|
};
|
|
}
|
|
|
|
export async function testEnvironment(ctx) {
|
|
const config = ctx.config ?? {};
|
|
const command = asString(config.hermesCommand) || HERMES_CLI;
|
|
const profileHome = asString(config.hermesProfileHome) || DEFAULT_PROFILE_HOME;
|
|
const checks = [];
|
|
|
|
const cliCheck = await checkCliInstalled(command);
|
|
if (cliCheck) {
|
|
checks.push(cliCheck);
|
|
if (cliCheck.level === "error") {
|
|
return {
|
|
adapterType: ADAPTER_TYPE,
|
|
status: "fail",
|
|
checks,
|
|
testedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
}
|
|
|
|
const profileCheck = await checkProfile(profileHome);
|
|
checks.push(profileCheck);
|
|
if (profileCheck.level === "error") {
|
|
return {
|
|
adapterType: ADAPTER_TYPE,
|
|
status: "fail",
|
|
checks,
|
|
testedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
const apiKeyCheck = await checkApiKey(profileHome, config.env);
|
|
checks.push(apiKeyCheck);
|
|
|
|
const model = asString(config.model);
|
|
checks.push({
|
|
level: "info",
|
|
message: model ? `Model: ${model}` : "Using profile default model",
|
|
code: "deepseek_model",
|
|
});
|
|
|
|
const hasErrors = checks.some((c) => c.level === "error");
|
|
const hasWarnings = checks.some((c) => c.level === "warn");
|
|
return {
|
|
adapterType: ADAPTER_TYPE,
|
|
status: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
|
|
checks,
|
|
testedAt: new Date().toISOString(),
|
|
};
|
|
}
|