mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
feat(providers): remove custom providers from settings
Add a destructive removal flow so mistaken custom providers can be deleted from config.json instead of only disabled.
This commit is contained in:
@@ -105,6 +105,7 @@ export interface ProviderSnapshotEntry {
|
||||
provider: AgentProvider;
|
||||
status: ProviderStatus;
|
||||
enabled: boolean;
|
||||
source?: "builtin" | "custom";
|
||||
error?: string;
|
||||
models?: AgentModelDefinition[];
|
||||
modes?: AgentMode[];
|
||||
|
||||
@@ -952,6 +952,9 @@ describe("ProviderSnapshotManager applyMutableProviderConfig", () => {
|
||||
expect(manager.hasProvider("zai-claude")).toBe(true);
|
||||
expect(state.providerDefinitions["zai-claude"]).toMatchObject({ enabled: true });
|
||||
expect(manager.listRegisteredProviderIds()).toContain("zai-claude");
|
||||
expect(manager.getSnapshot().find((entry) => entry.provider === "zai-claude")?.source).toBe(
|
||||
"custom",
|
||||
);
|
||||
} finally {
|
||||
manager.destroy();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
shutdownAgentClients,
|
||||
type ProviderDefinition,
|
||||
} from "./provider-registry.js";
|
||||
import { BUILTIN_PROVIDER_IDS } from "@getpaseo/protocol/provider-manifest";
|
||||
import { applyMutableProviderConfigToOverrides } from "../daemon-config-store.js";
|
||||
import {
|
||||
formatProviderDiagnostic,
|
||||
@@ -503,6 +504,7 @@ export class ProviderSnapshotManager {
|
||||
provider,
|
||||
status: "error",
|
||||
enabled: definition.enabled,
|
||||
source: this.getProviderSource(provider),
|
||||
label: definition.label,
|
||||
description: definition.description,
|
||||
defaultModeId: definition.defaultModeId,
|
||||
@@ -536,6 +538,11 @@ export class ProviderSnapshotManager {
|
||||
}
|
||||
}
|
||||
|
||||
private getProviderSource(provider: AgentProvider): ProviderSnapshotEntry["source"] {
|
||||
const isBuiltin = BUILTIN_PROVIDER_IDS.includes(provider);
|
||||
return !isBuiltin && this.providerOverrides?.[provider]?.extends ? "custom" : "builtin";
|
||||
}
|
||||
|
||||
private createLoadingEntries(): Map<AgentProvider, ProviderSnapshotEntry> {
|
||||
const entries = new Map<AgentProvider, ProviderSnapshotEntry>();
|
||||
for (const provider of this.getProviderIds()) {
|
||||
@@ -544,6 +551,7 @@ export class ProviderSnapshotManager {
|
||||
provider,
|
||||
status: "loading",
|
||||
enabled: definition?.enabled ?? true,
|
||||
source: this.getProviderSource(provider),
|
||||
label: definition?.label,
|
||||
description: definition?.description,
|
||||
defaultModeId: definition?.defaultModeId ?? null,
|
||||
@@ -562,6 +570,7 @@ export class ProviderSnapshotManager {
|
||||
const metadata = {
|
||||
provider,
|
||||
enabled: definition?.enabled ?? true,
|
||||
source: this.getProviderSource(provider),
|
||||
label: definition?.label,
|
||||
description: definition?.description,
|
||||
defaultModeId: definition?.defaultModeId ?? null,
|
||||
@@ -725,6 +734,7 @@ export class ProviderSnapshotManager {
|
||||
const snapshot = this.getOrCreateSnapshot(snapshotCwd);
|
||||
const base = {
|
||||
provider,
|
||||
source: this.getProviderSource(provider),
|
||||
label: definition.label,
|
||||
description: definition.description,
|
||||
defaultModeId: definition.defaultModeId,
|
||||
|
||||
@@ -119,6 +119,105 @@ describe("DaemonConfigStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("patch removes provider entries from config.json", () => {
|
||||
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
|
||||
tempDirs.push(paseoHome);
|
||||
|
||||
const configPath = path.join(paseoHome, "config.json");
|
||||
writeFileSync(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
agents: {
|
||||
providers: {
|
||||
gemini: {
|
||||
extends: "acp",
|
||||
label: "Gemini",
|
||||
command: ["gemini", "--acp"],
|
||||
},
|
||||
claude: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
const store = new DaemonConfigStore(
|
||||
paseoHome,
|
||||
{
|
||||
mcp: { injectIntoAgents: false },
|
||||
browserTools: { enabled: false },
|
||||
providers: {
|
||||
gemini: {},
|
||||
claude: { enabled: false },
|
||||
},
|
||||
metadataGeneration: { providers: [] },
|
||||
autoArchiveAfterMerge: false,
|
||||
enableTerminalAgentHooks: false,
|
||||
appendSystemPrompt: "",
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
const next = store.patch({ removeProviders: ["gemini"] });
|
||||
|
||||
expect(next.providers.gemini).toBeUndefined();
|
||||
expect(next.providers.claude).toEqual({ enabled: false });
|
||||
const persisted = loadPersistedConfig(paseoHome);
|
||||
expect(persisted.agents?.providers?.gemini).toBeUndefined();
|
||||
expect(persisted.agents?.providers?.claude).toEqual({ enabled: false });
|
||||
});
|
||||
|
||||
test("patch removes the providers object when the last provider is deleted", () => {
|
||||
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
|
||||
tempDirs.push(paseoHome);
|
||||
|
||||
const configPath = path.join(paseoHome, "config.json");
|
||||
writeFileSync(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
agents: {
|
||||
providers: {
|
||||
gemini: {
|
||||
extends: "acp",
|
||||
label: "Gemini",
|
||||
command: ["gemini", "--acp"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
const store = new DaemonConfigStore(
|
||||
paseoHome,
|
||||
{
|
||||
mcp: { injectIntoAgents: false },
|
||||
browserTools: { enabled: false },
|
||||
providers: { gemini: {} },
|
||||
metadataGeneration: { providers: [] },
|
||||
autoArchiveAfterMerge: false,
|
||||
enableTerminalAgentHooks: false,
|
||||
appendSystemPrompt: "",
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
store.patch({ removeProviders: ["gemini"] });
|
||||
|
||||
const persisted = loadPersistedConfig(paseoHome);
|
||||
expect(persisted.agents?.providers).toBeUndefined();
|
||||
});
|
||||
|
||||
test("patch persists append system prompt into config.json", () => {
|
||||
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
|
||||
tempDirs.push(paseoHome);
|
||||
|
||||
@@ -52,6 +52,53 @@ function deepMerge<T extends Record<string, unknown>>(
|
||||
return next as T;
|
||||
}
|
||||
|
||||
function omitProvidersFromConfig<T extends { providers?: Record<string, unknown> }>(
|
||||
config: T,
|
||||
providers: readonly string[],
|
||||
): T {
|
||||
if (providers.length === 0 || !config.providers) {
|
||||
return config;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const nextProviders = { ...config.providers };
|
||||
for (const provider of providers) {
|
||||
if (provider in nextProviders) {
|
||||
delete nextProviders[provider];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? ({ ...config, providers: nextProviders } as T) : config;
|
||||
}
|
||||
|
||||
function omitProvidersFromOverrides(
|
||||
overrides: Record<string, ProviderOverride> | undefined,
|
||||
providers: readonly string[],
|
||||
): Record<string, ProviderOverride> | undefined {
|
||||
if (!overrides) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nextOverrides = { ...overrides };
|
||||
for (const provider of providers) {
|
||||
delete nextOverrides[provider];
|
||||
}
|
||||
|
||||
return Object.keys(nextOverrides).length > 0 ? nextOverrides : undefined;
|
||||
}
|
||||
|
||||
function omitProvidersFromPersistedAgents(
|
||||
agents: PersistedConfig["agents"],
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!agents) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { providers: _providers, ...rest } = agents as Record<string, unknown>;
|
||||
return Object.keys(rest).length > 0 ? rest : undefined;
|
||||
}
|
||||
|
||||
function getValueAtPath(config: MutableDaemonConfig, path: string): unknown {
|
||||
return path
|
||||
.split(".")
|
||||
@@ -100,7 +147,9 @@ export class DaemonConfigStore {
|
||||
|
||||
public patch(partial: MutableDaemonConfigPatch): MutableDaemonConfig {
|
||||
const parsedPatch = MutableDaemonConfigPatchSchema.parse(partial);
|
||||
const next = MutableDaemonConfigSchema.parse(deepMerge(this.current, parsedPatch));
|
||||
const { removeProviders = [], ...configPatch } = parsedPatch;
|
||||
const merged = deepMerge(this.current, configPatch);
|
||||
const next = MutableDaemonConfigSchema.parse(omitProvidersFromConfig(merged, removeProviders));
|
||||
|
||||
const changedFieldPaths = Array.from(this.fieldChangeHandlers.keys()).filter((path) => {
|
||||
return !isEqualValue(getValueAtPath(this.current, path), getValueAtPath(next, path));
|
||||
@@ -112,7 +161,7 @@ export class DaemonConfigStore {
|
||||
|
||||
// Persist before updating in-memory state so that if persistence fails,
|
||||
// runtime and disk stay consistent.
|
||||
this.persistConfig(next);
|
||||
this.persistConfig(next, removeProviders);
|
||||
this.current = next;
|
||||
|
||||
for (const path of changedFieldPaths) {
|
||||
@@ -157,11 +206,12 @@ export class DaemonConfigStore {
|
||||
};
|
||||
}
|
||||
|
||||
private persistConfig(config: MutableDaemonConfig): void {
|
||||
private persistConfig(config: MutableDaemonConfig, removeProviders: readonly string[]): void {
|
||||
const persisted = loadPersistedConfig(this.paseoHome, this.logger);
|
||||
const nextPersisted = mergeMutableConfigIntoPersistedConfig({
|
||||
persisted,
|
||||
mutable: config,
|
||||
removeProviders,
|
||||
});
|
||||
savePersistedConfig(this.paseoHome, nextPersisted, this.logger);
|
||||
}
|
||||
@@ -170,22 +220,27 @@ export class DaemonConfigStore {
|
||||
function mergeMutableConfigIntoPersistedConfig(params: {
|
||||
persisted: PersistedConfig;
|
||||
mutable: MutableDaemonConfig;
|
||||
removeProviders: readonly string[];
|
||||
}): PersistedConfig {
|
||||
const { persisted, mutable } = params;
|
||||
const { persisted, mutable, removeProviders } = params;
|
||||
const browserToolsEnabled = readBrowserToolsEnabled(mutable);
|
||||
const metadataGenerationProviders = readMetadataGenerationProviders(mutable);
|
||||
const providerOverrides = applyMutableProviderConfigToOverrides(
|
||||
const persistedProviderOverrides = omitProvidersFromOverrides(
|
||||
persisted.agents?.providers as Record<string, ProviderOverride> | undefined,
|
||||
removeProviders,
|
||||
);
|
||||
const providerOverrides = applyMutableProviderConfigToOverrides(
|
||||
persistedProviderOverrides,
|
||||
mutable.providers,
|
||||
);
|
||||
const persistedAgents = persisted.agents as Record<string, unknown> | undefined;
|
||||
const persistedAgents = omitProvidersFromPersistedAgents(persisted.agents);
|
||||
const persistedMetadataGeneration = {
|
||||
providers: metadataGenerationProviders,
|
||||
};
|
||||
const shouldPersistMetadataGeneration =
|
||||
metadataGenerationProviders.length > 0 || persisted.agents?.metadataGeneration !== undefined;
|
||||
|
||||
let nextAgents = persisted.agents as PersistedConfig["agents"];
|
||||
let nextAgents = persistedAgents as PersistedConfig["agents"];
|
||||
if (providerOverrides && Object.keys(providerOverrides).length > 0) {
|
||||
nextAgents = {
|
||||
...persistedAgents,
|
||||
|
||||
@@ -1252,6 +1252,8 @@ export class VoiceAssistantWebSocketServer {
|
||||
workspaceGithubRepositorySearch: true,
|
||||
// COMPAT(projectCreateDirectory): added in v0.1.108, remove gate after 2027-01-15.
|
||||
projectCreateDirectory: true,
|
||||
// COMPAT(providerRemoval): added in v0.1.105, drop the gate when floor >= v0.1.105.
|
||||
providerRemoval: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user