feat: LLM-powered model auto-configuration and improved onboarding

Major changes:
- Add autoconfigure_models tool for intelligent model assignment
- Implement LLM-based model selection using openclaw agent
- Improve onboarding flow with better model access checks
- Update README with clearer installation and onboarding instructions

Technical improvements:
- Add model-fetcher utility to query authenticated models
- Add smart-model-selector for LLM-driven model assignment
- Use session context for LLM calls during onboarding
- Suppress logging from openclaw models list calls

Documentation:
- Add prerequisites section to README
- Add conversational onboarding example
- Improve quick start flow

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Lauren ten Hoor
2026-02-12 20:37:15 +08:00
parent 84483176f4
commit b2fc94db9e
12 changed files with 835 additions and 304 deletions

View File

@@ -0,0 +1,157 @@
/**
* llm-model-selector.ts — LLM-powered intelligent model selection.
*
* Uses an LLM to understand model capabilities and assign optimal models to DevClaw roles.
*/
import { execSync } from "node:child_process";
import { writeFileSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
export type ModelAssignment = {
dev: {
junior: string;
medior: string;
senior: string;
};
qa: {
reviewer: string;
tester: string;
};
};
/**
* Use an LLM to intelligently select and assign models to DevClaw roles.
*/
export async function selectModelsWithLLM(
availableModels: Array<{ model: string; provider: string }>,
sessionKey?: string,
): Promise<ModelAssignment | null> {
if (availableModels.length === 0) {
return null;
}
// If only one model, assign it to all roles
if (availableModels.length === 1) {
const model = availableModels[0].model;
return {
dev: { junior: model, medior: model, senior: model },
qa: { reviewer: model, tester: model },
};
}
// Create a prompt for the LLM
const modelList = availableModels.map((m) => m.model).join("\n");
const prompt = `You are an AI model expert. Analyze the following authenticated AI models and assign them to DevClaw development roles based on their capabilities.
Available models:
${modelList}
Assign models to these roles based on capability:
- **senior** (most capable): Complex architecture, refactoring, critical decisions
- **medior** (balanced): Features, bug fixes, code review
- **junior** (fast/efficient): Simple fixes, testing, routine tasks
- **reviewer** (same as medior): Code review
- **tester** (same as junior): Testing
Rules:
1. Prefer same provider for consistency
2. Assign most capable model to senior
3. Assign mid-tier model to medior/reviewer
4. Assign fastest/cheapest model to junior/tester
5. Consider model version numbers (higher = newer/better)
6. Stable versions (no date) > snapshot versions (with date like 20250514)
Return ONLY a JSON object in this exact format (no markdown, no explanation):
{
"dev": {
"junior": "provider/model-name",
"medior": "provider/model-name",
"senior": "provider/model-name"
},
"qa": {
"reviewer": "provider/model-name",
"tester": "provider/model-name"
}
}`;
// Write prompt to temp file for safe passing to shell
const tmpFile = join(tmpdir(), `devclaw-model-select-${Date.now()}.txt`);
writeFileSync(tmpFile, prompt, "utf-8");
try {
// Call openclaw agent using current session context if available
const sessionFlag = sessionKey
? `--session-id "${sessionKey}"`
: `--session-id devclaw-model-selection`;
const result = execSync(
`openclaw agent --local ${sessionFlag} --message "$(cat ${tmpFile})" --json`,
{
encoding: "utf-8",
timeout: 30000,
stdio: ["pipe", "pipe", "ignore"],
},
).trim();
// Parse the response from openclaw agent --json
const lines = result.split("\n");
const jsonStartIndex = lines.findIndex((line) => line.trim().startsWith("{"));
if (jsonStartIndex === -1) {
throw new Error("No JSON found in LLM response");
}
const jsonString = lines.slice(jsonStartIndex).join("\n");
// openclaw agent --json returns: { payloads: [{ text: "```json\n{...}\n```" }], meta: {...} }
const response = JSON.parse(jsonString);
if (!response.payloads || !Array.isArray(response.payloads) || response.payloads.length === 0) {
throw new Error("Invalid openclaw agent response structure - missing payloads");
}
// Extract text from first payload
const textContent = response.payloads[0].text;
if (!textContent) {
throw new Error("Empty text content in openclaw agent payload");
}
// Strip markdown code blocks (```json and ```)
const cleanJson = textContent
.replace(/```json\n?/g, '')
.replace(/```\n?/g, '')
.trim();
// Parse the actual model assignment JSON
const assignment = JSON.parse(cleanJson);
// Log what we got for debugging
console.log("LLM returned:", JSON.stringify(assignment, null, 2));
// Validate the structure
if (
!assignment.dev?.junior ||
!assignment.dev?.medior ||
!assignment.dev?.senior ||
!assignment.qa?.reviewer ||
!assignment.qa?.tester
) {
console.error("Invalid assignment structure. Got:", assignment);
throw new Error(`Invalid assignment structure from LLM. Missing fields in: ${JSON.stringify(Object.keys(assignment))}`);
}
return assignment as ModelAssignment;
} catch (err) {
console.error("LLM model selection failed:", (err as Error).message);
return null;
} finally {
// Clean up temp file
try {
unlinkSync(tmpFile);
} catch {
// Ignore cleanup errors
}
}
}

View File

@@ -0,0 +1,81 @@
/**
* model-fetcher.ts — Shared helper for fetching OpenClaw models without logging.
*
* Uses execSync to bypass OpenClaw's command logging infrastructure.
*/
import { execSync } from "node:child_process";
export type OpenClawModelRow = {
key: string;
name?: string;
input: string;
contextWindow: number | null;
local: boolean;
available: boolean;
tags: string[];
missing?: boolean;
};
/**
* Fetch all models from OpenClaw without logging.
*
* @param allModels - If true, fetches all models (--all flag). If false, only authenticated models.
* @returns Array of model objects from OpenClaw's model registry
*/
export function fetchModels(allModels = true): OpenClawModelRow[] {
try {
const command = allModels
? "openclaw models list --all --json"
: "openclaw models list --json";
// Use execSync directly to bypass OpenClaw's command logging
const output = execSync(command, {
encoding: "utf-8",
timeout: 10000,
cwd: process.cwd(),
// Suppress stderr to avoid any error messages
stdio: ["pipe", "pipe", "ignore"],
}).trim();
if (!output) {
throw new Error("Empty output from openclaw models list");
}
// Parse JSON (skip any log lines like "[plugins] ...")
const lines = output.split("\n");
// Find the first line that starts with { (the beginning of JSON)
const jsonStartIndex = lines.findIndex((line: string) => {
const trimmed = line.trim();
return trimmed.startsWith("{");
});
if (jsonStartIndex === -1) {
throw new Error(
`No JSON object found in output. Got: ${output.substring(0, 200)}...`,
);
}
// Join all lines from the JSON start to the end
const jsonString = lines.slice(jsonStartIndex).join("\n");
const data = JSON.parse(jsonString);
const models = data.models as OpenClawModelRow[];
if (!Array.isArray(models)) {
throw new Error(`Expected array of models, got: ${typeof models}`);
}
return models;
} catch (err) {
throw new Error(`Failed to fetch models: ${(err as Error).message}`);
}
}
/**
* Fetch only authenticated models (available: true).
*/
export function fetchAuthenticatedModels(): OpenClawModelRow[] {
// Use --all flag but suppress logging via stdio in fetchModels()
return fetchModels(true).filter((m) => m.available === true);
}

View File

@@ -0,0 +1,98 @@
/**
* smart-model-selector.ts — LLM-powered model selection for DevClaw roles.
*
* Uses an LLM to intelligently analyze and assign models to DevClaw roles.
*/
export type ModelAssignment = {
dev: {
junior: string;
medior: string;
senior: string;
};
qa: {
reviewer: string;
tester: string;
};
};
/**
* Intelligently assign available models to DevClaw roles using an LLM.
*
* Strategy:
* 1. If 0 models → return null (setup should be blocked)
* 2. If 1 model → assign it to all roles
* 3. If multiple models → use LLM to intelligently assign
*/
export async function assignModels(
availableModels: Array<{ model: string; provider: string; authenticated: boolean }>,
sessionKey?: string,
): Promise<ModelAssignment | null> {
// Filter to only authenticated models
const authenticated = availableModels.filter((m) => m.authenticated);
if (authenticated.length === 0) {
return null; // No models available - setup should be blocked
}
// If only one model, use it for everything
if (authenticated.length === 1) {
const model = authenticated[0].model;
return {
dev: { junior: model, medior: model, senior: model },
qa: { reviewer: model, tester: model },
};
}
// Multiple models: use LLM-based selection
const { selectModelsWithLLM } = await import("./llm-model-selector.js");
const llmResult = await selectModelsWithLLM(authenticated, sessionKey);
if (!llmResult) {
throw new Error("LLM-based model selection failed. Please try again or configure models manually.");
}
return llmResult;
}
/**
* Format model assignment as a readable table.
*/
export function formatAssignment(assignment: ModelAssignment): string {
const lines = [
"| Role | Level | Model |",
"|------|----------|--------------------------|",
`| DEV | senior | ${assignment.dev.senior.padEnd(24)} |`,
`| DEV | medior | ${assignment.dev.medior.padEnd(24)} |`,
`| DEV | junior | ${assignment.dev.junior.padEnd(24)} |`,
`| QA | reviewer | ${assignment.qa.reviewer.padEnd(24)} |`,
`| QA | tester | ${assignment.qa.tester.padEnd(24)} |`,
];
return lines.join("\n");
}
/**
* Generate setup instructions when no models are available.
*/
export function generateSetupInstructions(): string {
return `❌ No authenticated models found. DevClaw needs at least one model to work.
To configure model authentication:
**For Anthropic Claude:**
export ANTHROPIC_API_KEY=your-api-key
# or: openclaw auth add --provider anthropic
**For OpenAI:**
export OPENAI_API_KEY=your-api-key
# or: openclaw auth add --provider openai
**For other providers:**
openclaw auth add --provider <provider>
**Verify authentication:**
openclaw models list
(Look for "Auth: yes" in the output)
Once you see authenticated models, re-run: onboard`;
}