feat: enable/disable providers from settings

Adds a per-provider toggle in Settings → Providers, persisted via daemon
config, so users can turn off providers they don't have installed (or
don't want to use) without removing them from the registry.

Disabled providers stay visible everywhere — settings, `paseo provider
ls`, MCP `list_providers`, model/mode lists — marked as disabled rather
than silently filtered. Availability probes are skipped for them, and
attempts to launch an agent or fetch models/modes from a disabled
provider return a clear "Provider '<id>' is disabled" error.

Schema additions (`enabled` on `ProviderSnapshotEntry`, MCP provider
summary) are backward-compatible: optional with a default of true so
older clients continue to parse new daemon messages.

CLI: `paseo provider ls` gains an ENABLED column; JSON output includes
the new field.
This commit is contained in:
Mohamed Boudra
2026-04-25 14:00:38 +07:00
parent de42858213
commit 4b3603604a
31 changed files with 1920 additions and 216 deletions
+91 -15
View File
@@ -20,7 +20,14 @@
*/
import assert from "node:assert";
import { createE2ETestContext } from "./helpers/test-daemon.ts";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
createE2ETestContext,
createTempDirs,
runPaseoCli,
startTestDaemon,
} from "./helpers/test-daemon.ts";
console.log("=== Provider Commands ===\n");
@@ -30,6 +37,13 @@ interface ProviderModel {
description?: string;
}
interface ProviderListRow {
provider: string;
label: string;
status: string;
enabled: string;
}
const EXPECTED_CLAUDE_MODELS = [
{
id: "claude-opus-4-7[1m]",
@@ -140,6 +154,8 @@ try {
assert(result.stdout.includes("claude"), "output should include claude");
assert(result.stdout.includes("codex"), "output should include codex");
assert(result.stdout.includes("opencode"), "output should include opencode");
assert(result.stdout.includes("ENABLED"), "output should include ENABLED column");
assert(result.stdout.includes("Enabled"), "output should show enabled providers");
assert(
result.stdout.includes("available") ||
result.stdout.includes("loading") ||
@@ -169,12 +185,72 @@ try {
data.some((p: { provider: string }) => p.provider === "opencode"),
"should include opencode",
);
assert(
data.every((p: ProviderListRow) => p.enabled === "Enabled"),
"enabled providers should report Enabled",
);
console.log("✓ provider ls --json outputs valid JSON\n");
}
// Test 4: provider ls --quiet outputs provider names only
// Test 4: provider ls includes disabled providers
{
console.log("Test 4: provider ls --quiet outputs provider names only");
console.log("Test 4: provider ls includes disabled providers");
const { paseoHome, workDir } = await createTempDirs();
await writeFile(
join(paseoHome, "config.json"),
JSON.stringify(
{
version: 1,
agents: {
providers: {
claude: {
enabled: false,
},
},
},
},
null,
2,
) + "\n",
);
const disabledCtx = await startTestDaemon({ paseoHome, workDir, timeout: 120000 });
try {
const result = await runPaseoCli(disabledCtx, ["provider", "ls", "--json"]);
assert.strictEqual(result.exitCode, 0, "provider ls should exit 0");
const data = JSON.parse(result.stdout.trim()) as ProviderListRow[];
const claude = data.find((p) => p.provider === "claude");
assert(claude, "disabled claude provider should stay in provider ls");
assert.strictEqual(claude.enabled, "Disabled", "disabled provider should report Disabled");
const opencode = data.find((p) => p.provider === "opencode");
assert(opencode, "enabled opencode provider should stay in provider ls");
assert.strictEqual(opencode.enabled, "Enabled", "enabled provider should report Enabled");
const modelsResult = await runPaseoCli(disabledCtx, ["provider", "models", "claude"]);
assert.notStrictEqual(
modelsResult.exitCode,
0,
"provider models should fail for disabled providers",
);
const output = modelsResult.stdout + modelsResult.stderr;
assert(
output.includes("Provider claude is not available"),
"provider models should surface the daemon disabled error",
);
assert(
!output.includes("claude-sonnet"),
"provider models should not print fallback models for disabled providers",
);
} finally {
await disabledCtx.stop();
}
console.log("✓ provider ls includes disabled providers\n");
}
// Test 5: provider ls --quiet outputs provider names only
{
console.log("Test 5: provider ls --quiet outputs provider names only");
const result = await ctx.paseo(["provider", "ls", "--quiet"]);
assert.strictEqual(result.exitCode, 0, "should exit 0");
const lines = result.stdout.trim().split("\n");
@@ -185,17 +261,17 @@ try {
console.log("✓ provider ls --quiet outputs provider names only\n");
}
// Test 5: provider models claude lists canonical model aliases
// Test 6: provider models claude lists canonical model aliases
{
console.log("Test 5: provider models claude lists canonical model aliases");
console.log("Test 6: provider models claude lists canonical model aliases");
const data = await runProviderModelsJson("claude");
assertClaudeModels(data);
console.log("✓ provider models claude lists canonical model aliases\n");
}
// Test 6: provider models codex includes concrete codex model IDs
// Test 7: provider models codex includes concrete codex model IDs
{
console.log("Test 6: provider models codex includes concrete codex model IDs");
console.log("Test 7: provider models codex includes concrete codex model IDs");
const data = await runProviderModelsJson("codex");
assert(data.length >= 1, "codex model list should not be empty");
const ids = data.map((m) => m.id);
@@ -215,9 +291,9 @@ try {
console.log("✓ provider models codex includes concrete codex model IDs\n");
}
// Test 7: provider models opencode returns namespaced model IDs
// Test 8: provider models opencode returns namespaced model IDs
{
console.log("Test 7: provider models opencode returns namespaced model IDs");
console.log("Test 8: provider models opencode returns namespaced model IDs");
const data = await runProviderModelsJson("opencode");
assert(data.length >= 1, "opencode model list should not be empty");
const ids = data.map((m) => m.id);
@@ -236,9 +312,9 @@ try {
console.log("✓ provider models opencode returns namespaced model IDs\n");
}
// Test 8: provider models unknown fails with error
// Test 9: provider models unknown fails with error
{
console.log("Test 8: provider models unknown fails with error");
console.log("Test 9: provider models unknown fails with error");
const result = await ctx.paseo(["provider", "models", "unknown"]);
assert.notStrictEqual(result.exitCode, 0, "should fail for unknown provider");
const output = result.stdout + result.stderr;
@@ -249,9 +325,9 @@ try {
console.log("✓ provider models unknown fails with error\n");
}
// Test 9: provider models --json outputs valid JSON
// Test 10: provider models --json outputs valid JSON
{
console.log("Test 9: provider models --json outputs valid JSON");
console.log("Test 10: provider models --json outputs valid JSON");
const data = await runProviderModelsJson("claude");
assert(Array.isArray(data), "output should be an array");
assert(
@@ -264,9 +340,9 @@ try {
console.log("✓ provider models --json outputs valid JSON\n");
}
// Test 10: provider models --quiet outputs model IDs only
// Test 11: provider models --quiet outputs model IDs only
{
console.log("Test 10: provider models --quiet outputs model IDs only");
console.log("Test 11: provider models --quiet outputs model IDs only");
assert(
claudeModelIdsFromJson.length > 0,
"claude model IDs should be captured from --json output",