Files
legal-ai/adapters/deepseek-paperclip-adapter/dist/server/test.js
Chaim 45341a0bc8 feat(curator): switch Hermes Curator to DeepSeek V4-Pro via deepseek_local adapter
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>
2026-05-10 05:58:52 +00:00

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(),
};
}