refactor: extract arcee provider cleanup seams

This commit is contained in:
Peter Steinberger
2026-04-06 18:29:53 +01:00
parent 177be0f237
commit b7d3a26356
7 changed files with 133 additions and 100 deletions

View File

@@ -1,7 +1,11 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared";
import {
readConfiguredProviderCatalogEntries,
type ProviderCatalogContext,
} from "openclaw/plugin-sdk/provider-catalog-shared";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard";
import {
applyArceeConfig,
applyArceeOpenRouterConfig,
@@ -19,6 +23,88 @@ const PROVIDER_ID = "arcee";
const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
family: "openai-compatible",
});
const ARCEE_WIZARD_GROUP = {
groupId: "arcee",
groupLabel: "Arcee AI",
groupHint: "Direct API or OpenRouter",
} as const;
function buildArceeAuthMethods() {
return [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "arcee-platform",
label: "Arcee AI API key",
hint: "Direct access to Arcee platform",
optionKey: "arceeaiApiKey",
flagName: "--arceeai-api-key",
envVar: "ARCEEAI_API_KEY",
promptMessage: "Enter Arcee AI API key",
defaultModel: ARCEE_DEFAULT_MODEL_REF,
expectedProviders: [PROVIDER_ID],
applyConfig: (cfg) => applyArceeConfig(cfg),
wizard: {
choiceId: "arceeai-api-key",
choiceLabel: "Arcee AI API key",
choiceHint: "Direct (chat.arcee.ai)",
...ARCEE_WIZARD_GROUP,
},
}),
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "openrouter",
label: "OpenRouter API key",
hint: "Access Arcee models via OpenRouter",
optionKey: "openrouterApiKey",
flagName: "--openrouter-api-key",
envVar: "OPENROUTER_API_KEY",
promptMessage: "Enter OpenRouter API key",
profileId: "openrouter:default",
defaultModel: ARCEE_OPENROUTER_DEFAULT_MODEL_REF,
expectedProviders: [PROVIDER_ID, "openrouter"],
applyConfig: (cfg) => applyArceeOpenRouterConfig(cfg),
wizard: {
choiceId: "arceeai-openrouter",
choiceLabel: "OpenRouter API key",
choiceHint: "Via OpenRouter (openrouter.ai)",
...ARCEE_WIZARD_GROUP,
},
}),
];
}
function readConfiguredArceeCatalogEntries(config: OpenClawConfig | undefined) {
return readConfiguredProviderCatalogEntries({
config,
providerId: PROVIDER_ID,
});
}
async function resolveArceeCatalog(ctx: ProviderCatalogContext) {
const directKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (directKey) {
return { provider: { ...buildArceeProvider(), apiKey: directKey } };
}
const openRouterKey = ctx.resolveProviderApiKey("openrouter").apiKey;
if (openRouterKey) {
return { provider: { ...buildArceeOpenRouterProvider(), apiKey: openRouterKey } };
}
return null;
}
function normalizeArceeResolvedModel<T extends { baseUrl?: string; id: string }>(
model: T,
): T | undefined {
if (!isArceeOpenRouterBaseUrl(model.baseUrl)) {
return undefined;
}
return {
...model,
id: toArceeOpenRouterModelId(model.id),
};
}
export default definePluginEntry({
id: PROVIDER_ID,
@@ -30,76 +116,12 @@ export default definePluginEntry({
label: "Arcee AI",
docsPath: "/providers/arcee",
envVars: ["ARCEEAI_API_KEY", "OPENROUTER_API_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "arcee-platform",
label: "Arcee AI API key",
hint: "Direct access to Arcee platform",
optionKey: "arceeaiApiKey",
flagName: "--arceeai-api-key",
envVar: "ARCEEAI_API_KEY",
promptMessage: "Enter Arcee AI API key",
defaultModel: ARCEE_DEFAULT_MODEL_REF,
expectedProviders: [PROVIDER_ID],
applyConfig: (cfg) => applyArceeConfig(cfg),
wizard: {
choiceId: "arceeai-api-key",
choiceLabel: "Arcee AI API key",
choiceHint: "Direct (chat.arcee.ai)",
groupId: "arcee",
groupLabel: "Arcee AI",
groupHint: "Direct API or OpenRouter",
},
}),
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "openrouter",
label: "OpenRouter API key",
hint: "Access Arcee models via OpenRouter",
optionKey: "openrouterApiKey",
flagName: "--openrouter-api-key",
envVar: "OPENROUTER_API_KEY",
promptMessage: "Enter OpenRouter API key",
profileId: "openrouter:default",
defaultModel: ARCEE_OPENROUTER_DEFAULT_MODEL_REF,
expectedProviders: [PROVIDER_ID, "openrouter"],
applyConfig: (cfg) => applyArceeOpenRouterConfig(cfg),
wizard: {
choiceId: "arceeai-openrouter",
choiceLabel: "OpenRouter API key",
choiceHint: "Via OpenRouter (openrouter.ai)",
groupId: "arcee",
groupLabel: "Arcee AI",
groupHint: "Direct API or OpenRouter",
},
}),
],
auth: buildArceeAuthMethods(),
catalog: {
run: async (ctx) => {
const directKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (directKey) {
return { provider: { ...buildArceeProvider(), apiKey: directKey } };
}
const orKey = ctx.resolveProviderApiKey("openrouter").apiKey;
if (orKey) {
return { provider: { ...buildArceeOpenRouterProvider(), apiKey: orKey } };
}
return null;
},
run: resolveArceeCatalog,
},
augmentModelCatalog: ({ config }) =>
readConfiguredProviderCatalogEntries({
config,
providerId: PROVIDER_ID,
}),
normalizeResolvedModel: ({ model }) =>
isArceeOpenRouterBaseUrl(model.baseUrl)
? {
...model,
id: toArceeOpenRouterModelId(model.id),
}
: undefined,
augmentModelCatalog: ({ config }) => readConfiguredArceeCatalogEntries(config),
normalizeResolvedModel: ({ model }) => normalizeArceeResolvedModel(model),
...OPENAI_COMPATIBLE_REPLAY_HOOKS,
});
},

View File

@@ -2,8 +2,12 @@ import {
createModelCatalogPresetAppliers,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { buildArceeModelDefinition, ARCEE_BASE_URL, ARCEE_MODEL_CATALOG } from "./api.js";
import { OPENROUTER_BASE_URL, toArceeOpenRouterModelId } from "./provider-catalog.js";
import { ARCEE_BASE_URL } from "./api.js";
import {
buildArceeCatalogModels,
buildArceeOpenRouterCatalogModels,
OPENROUTER_BASE_URL,
} from "./provider-catalog.js";
export const ARCEE_DEFAULT_MODEL_REF = "arcee/trinity-large-thinking";
export const ARCEE_OPENROUTER_DEFAULT_MODEL_REF = "arcee/trinity-large-thinking";
@@ -14,7 +18,7 @@ const arceePresetAppliers = createModelCatalogPresetAppliers({
providerId: "arcee",
api: "openai-completions",
baseUrl: ARCEE_BASE_URL,
catalogModels: ARCEE_MODEL_CATALOG.map(buildArceeModelDefinition),
catalogModels: buildArceeCatalogModels(),
aliases: [{ modelRef: ARCEE_DEFAULT_MODEL_REF, alias: "Arcee AI" }],
}),
});
@@ -25,10 +29,7 @@ const arceeOpenRouterPresetAppliers = createModelCatalogPresetAppliers({
providerId: "arcee",
api: "openai-completions",
baseUrl: OPENROUTER_BASE_URL,
catalogModels: ARCEE_MODEL_CATALOG.map((model) => ({
...buildArceeModelDefinition(model),
id: toArceeOpenRouterModelId(model.id),
})),
catalogModels: buildArceeOpenRouterCatalogModels(),
aliases: [{ modelRef: ARCEE_OPENROUTER_DEFAULT_MODEL_REF, alias: "Arcee AI (OpenRouter)" }],
}),
});

View File

@@ -21,11 +21,22 @@ export function toArceeOpenRouterModelId(modelId: string): string {
return `arcee/${normalized}`;
}
export function buildArceeCatalogModels(): NonNullable<ModelProviderConfig["models"]> {
return ARCEE_MODEL_CATALOG.map(buildArceeModelDefinition);
}
export function buildArceeOpenRouterCatalogModels(): NonNullable<ModelProviderConfig["models"]> {
return buildArceeCatalogModels().map((model) => ({
...model,
id: toArceeOpenRouterModelId(model.id),
}));
}
export function buildArceeProvider(): ModelProviderConfig {
return {
baseUrl: ARCEE_BASE_URL,
api: "openai-completions",
models: ARCEE_MODEL_CATALOG.map(buildArceeModelDefinition),
models: buildArceeCatalogModels(),
};
}
@@ -33,9 +44,6 @@ export function buildArceeOpenRouterProvider(): ModelProviderConfig {
return {
baseUrl: OPENROUTER_BASE_URL,
api: "openai-completions",
models: ARCEE_MODEL_CATALOG.map((model) => ({
...buildArceeModelDefinition(model),
id: toArceeOpenRouterModelId(model.id),
})),
models: buildArceeOpenRouterCatalogModels(),
};
}

View File

@@ -12,8 +12,8 @@ export type BuiltInAuthChoice =
"oauth" | "setup-token" | "token" | "apiKey" | "custom-api-key" | "skip";
export type AuthChoice = BuiltInAuthChoice | (string & {});
export type BuiltInAuthChoiceGroupId = "custom";
export type AuthChoiceGroupId = BuiltInAuthChoiceGroupId | (string & {});
/** Auth choice groups are plugin-owned ids plus the core `custom` bucket. */
export type AuthChoiceGroupId = "custom" | (string & {});
export type GatewayAuthChoice = "token" | "password";
export type ResetScope = "config" | "config+creds+sessions" | "full";
export type GatewayBind = "loopback" | "lan" | "auto" | "custom" | "tailnet";

View File

@@ -15,7 +15,7 @@ describe("config io shell env expected keys", () => {
]);
vi.resetModules();
const { resolveShellEnvExpectedKeys } = await import("./io.js");
const { resolveShellEnvExpectedKeys } = await import("./shell-env-expected-keys.js");
expect(resolveShellEnvExpectedKeys({} as NodeJS.ProcessEnv)).toEqual(
expect.arrayContaining([

View File

@@ -14,7 +14,6 @@ import {
shouldEnableShellEnvFallback,
} from "../infra/shell-env.js";
import { listPluginDoctorLegacyConfigRules } from "../plugins/doctor-contract-registry.js";
import { listKnownProviderAuthEnvVarNames } from "../secrets/provider-env-vars.js";
import { sanitizeTerminalText } from "../terminal/safe-text.js";
import { VERSION } from "../version.js";
import { DuplicateAgentDirError, findDuplicateAgentDirs } from "./agent-dirs.js";
@@ -51,6 +50,7 @@ import {
setRuntimeConfigSnapshot as setRuntimeConfigSnapshotState,
setRuntimeConfigSnapshotRefreshHandler as setRuntimeConfigSnapshotRefreshHandlerState,
} from "./runtime-snapshot.js";
import { resolveShellEnvExpectedKeys } from "./shell-env-expected-keys.js";
import type { OpenClawConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
import {
validateConfigObjectRawWithPlugins,
@@ -70,21 +70,7 @@ export {
// Re-export for backwards compatibility
export { CircularIncludeError, ConfigIncludeError } from "./includes.js";
export { MissingEnvVarError } from "./env-substitution.js";
const CORE_SHELL_ENV_EXPECTED_KEYS = [
"TELEGRAM_BOT_TOKEN",
"DISCORD_BOT_TOKEN",
"SLACK_BOT_TOKEN",
"SLACK_APP_TOKEN",
"OPENCLAW_GATEWAY_TOKEN",
"OPENCLAW_GATEWAY_PASSWORD",
];
export function resolveShellEnvExpectedKeys(env: NodeJS.ProcessEnv): string[] {
return [
...new Set([...listKnownProviderAuthEnvVarNames({ env }), ...CORE_SHELL_ENV_EXPECTED_KEYS]),
];
}
export { resolveShellEnvExpectedKeys } from "./shell-env-expected-keys.js";
const OPEN_DM_POLICY_ALLOW_FROM_RE =
/^(?<policyPath>[a-z0-9_.-]+)\s*=\s*"open"\s+requires\s+(?<allowPath>[a-z0-9_.-]+)(?:\s+\(or\s+[a-z0-9_.-]+\))?\s+to include "\*"$/i;

View File

@@ -0,0 +1,16 @@
import { listKnownProviderAuthEnvVarNames } from "../secrets/provider-env-vars.js";
const CORE_SHELL_ENV_EXPECTED_KEYS = [
"TELEGRAM_BOT_TOKEN",
"DISCORD_BOT_TOKEN",
"SLACK_BOT_TOKEN",
"SLACK_APP_TOKEN",
"OPENCLAW_GATEWAY_TOKEN",
"OPENCLAW_GATEWAY_PASSWORD",
];
export function resolveShellEnvExpectedKeys(env: NodeJS.ProcessEnv): string[] {
return [
...new Set([...listKnownProviderAuthEnvVarNames({ env }), ...CORE_SHELL_ENV_EXPECTED_KEYS]),
];
}