From bcfddcc768b11bec81d89fea198f2ffc74483441 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 27 Mar 2026 12:25:55 +0000 Subject: [PATCH] refactor: pluginize litellm auth onboarding --- extensions/litellm/index.ts | 41 ++++++++ extensions/litellm/onboard.ts | 55 +++++++++++ extensions/litellm/openclaw.plugin.json | 28 ++++++ extensions/litellm/package.json | 12 +++ extensions/litellm/provider-catalog.ts | 10 ++ src/commands/auth-choice-options.static.ts | 15 --- src/commands/auth-choice-options.test.ts | 69 ++++++++++++- .../auth-choice.apply.api-key-providers.ts | 96 ------------------- .../auth-choice.apply.api-providers.ts | 51 +--------- src/commands/auth-choice.test.ts | 2 + src/commands/onboard-auth.config-litellm.ts | 65 ------------- src/commands/onboard-auth.test.ts | 2 +- src/commands/onboard-core-auth-flags.ts | 10 +- ...oard-non-interactive.provider-auth.test.ts | 3 + .../local/auth-choice.api-key-providers.ts | 67 ------------- .../local/auth-choice.test.ts | 10 +- .../local/auth-choice.ts | 15 --- .../bundled-plugin-entries.generated.ts | 3 + .../bundled-plugin-metadata.generated.ts | 42 ++++++++ ...undled-provider-auth-env-vars.generated.ts | 1 + .../provider-auth-choice-preference.ts | 2 - 21 files changed, 271 insertions(+), 328 deletions(-) create mode 100644 extensions/litellm/index.ts create mode 100644 extensions/litellm/onboard.ts create mode 100644 extensions/litellm/openclaw.plugin.json create mode 100644 extensions/litellm/package.json create mode 100644 extensions/litellm/provider-catalog.ts delete mode 100644 src/commands/auth-choice.apply.api-key-providers.ts delete mode 100644 src/commands/onboard-auth.config-litellm.ts delete mode 100644 src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts diff --git a/extensions/litellm/index.ts b/extensions/litellm/index.ts new file mode 100644 index 00000000000..a2bb3d5e9bd --- /dev/null +++ b/extensions/litellm/index.ts @@ -0,0 +1,41 @@ +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; +import { applyLitellmConfig, LITELLM_DEFAULT_MODEL_REF } from "./onboard.js"; +import { buildLitellmProvider } from "./provider-catalog.js"; + +const PROVIDER_ID = "litellm"; + +export default defineSingleProviderPluginEntry({ + id: PROVIDER_ID, + name: "LiteLLM Provider", + description: "Bundled LiteLLM provider plugin", + provider: { + label: "LiteLLM", + docsPath: "/providers/litellm", + auth: [ + { + methodId: "api-key", + label: "LiteLLM API key", + hint: "Unified gateway for 100+ LLM providers", + optionKey: "litellmApiKey", + flagName: "--litellm-api-key", + envVar: "LITELLM_API_KEY", + promptMessage: "Enter LiteLLM API key", + defaultModel: LITELLM_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyLitellmConfig(cfg), + noteTitle: "LiteLLM", + noteMessage: [ + "LiteLLM provides a unified API to 100+ LLM providers.", + "Get your API key from your LiteLLM proxy or https://litellm.ai", + "Default proxy runs on http://localhost:4000", + ].join("\n"), + wizard: { + groupHint: "Unified LLM gateway (100+ providers)", + }, + }, + ], + catalog: { + buildProvider: buildLitellmProvider, + allowExplicitBaseUrl: true, + }, + }, +}); diff --git a/extensions/litellm/onboard.ts b/extensions/litellm/onboard.ts new file mode 100644 index 00000000000..f6f1c169faa --- /dev/null +++ b/extensions/litellm/onboard.ts @@ -0,0 +1,55 @@ +import { + createDefaultModelPresetAppliers, + type ModelDefinitionConfig, + type OpenClawConfig, +} from "openclaw/plugin-sdk/provider-onboard"; + +export const LITELLM_BASE_URL = "http://localhost:4000"; +export const LITELLM_DEFAULT_MODEL_ID = "claude-opus-4-6"; +export const LITELLM_DEFAULT_MODEL_REF = `litellm/${LITELLM_DEFAULT_MODEL_ID}`; +const LITELLM_DEFAULT_CONTEXT_WINDOW = 128_000; +const LITELLM_DEFAULT_MAX_TOKENS = 8_192; +const LITELLM_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +export function buildLitellmModelDefinition(): ModelDefinitionConfig { + return { + id: LITELLM_DEFAULT_MODEL_ID, + name: "Claude Opus 4.6", + reasoning: true, + input: ["text", "image"], + cost: LITELLM_DEFAULT_COST, + contextWindow: LITELLM_DEFAULT_CONTEXT_WINDOW, + maxTokens: LITELLM_DEFAULT_MAX_TOKENS, + }; +} + +const litellmPresetAppliers = createDefaultModelPresetAppliers({ + primaryModelRef: LITELLM_DEFAULT_MODEL_REF, + resolveParams: (cfg: OpenClawConfig) => { + const existingProvider = cfg.models?.providers?.litellm as { baseUrl?: unknown } | undefined; + const resolvedBaseUrl = + typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : ""; + + return { + providerId: "litellm", + api: "openai-completions" as const, + baseUrl: resolvedBaseUrl || LITELLM_BASE_URL, + defaultModel: buildLitellmModelDefinition(), + defaultModelId: LITELLM_DEFAULT_MODEL_ID, + aliases: [{ modelRef: LITELLM_DEFAULT_MODEL_REF, alias: "LiteLLM" }], + }; + }, +}); + +export function applyLitellmProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return litellmPresetAppliers.applyProviderConfig(cfg); +} + +export function applyLitellmConfig(cfg: OpenClawConfig): OpenClawConfig { + return litellmPresetAppliers.applyConfig(cfg); +} diff --git a/extensions/litellm/openclaw.plugin.json b/extensions/litellm/openclaw.plugin.json new file mode 100644 index 00000000000..e8a7b600569 --- /dev/null +++ b/extensions/litellm/openclaw.plugin.json @@ -0,0 +1,28 @@ +{ + "id": "litellm", + "providers": ["litellm"], + "providerAuthEnvVars": { + "litellm": ["LITELLM_API_KEY"] + }, + "providerAuthChoices": [ + { + "provider": "litellm", + "method": "api-key", + "choiceId": "litellm-api-key", + "choiceLabel": "LiteLLM API key", + "choiceHint": "Unified gateway for 100+ LLM providers", + "groupId": "litellm", + "groupLabel": "LiteLLM", + "groupHint": "Unified LLM gateway (100+ providers)", + "optionKey": "litellmApiKey", + "cliFlag": "--litellm-api-key", + "cliOption": "--litellm-api-key ", + "cliDescription": "LiteLLM API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/litellm/package.json b/extensions/litellm/package.json new file mode 100644 index 00000000000..189857ef8dd --- /dev/null +++ b/extensions/litellm/package.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/litellm-provider", + "version": "2026.3.26", + "private": true, + "description": "OpenClaw LiteLLM provider plugin", + "type": "module", + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/litellm/provider-catalog.ts b/extensions/litellm/provider-catalog.ts new file mode 100644 index 00000000000..6136cfaafca --- /dev/null +++ b/extensions/litellm/provider-catalog.ts @@ -0,0 +1,10 @@ +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-models"; +import { buildLitellmModelDefinition, LITELLM_BASE_URL } from "./onboard.js"; + +export function buildLitellmProvider(): ModelProviderConfig { + return { + baseUrl: LITELLM_BASE_URL, + api: "openai-completions", + models: [buildLitellmModelDefinition()], + }; +} diff --git a/src/commands/auth-choice-options.static.ts b/src/commands/auth-choice-options.static.ts index 2dd92e35cdf..817a683890b 100644 --- a/src/commands/auth-choice-options.static.ts +++ b/src/commands/auth-choice-options.static.ts @@ -20,21 +20,6 @@ export type AuthChoiceGroup = { }; export const CORE_AUTH_CHOICE_OPTIONS: ReadonlyArray = [ - { - value: "chutes", - label: "Chutes (OAuth)", - groupId: "chutes", - groupLabel: "Chutes", - groupHint: "OAuth", - }, - { - value: "litellm-api-key", - label: "LiteLLM API key", - hint: "Unified gateway for 100+ LLM providers", - groupId: "litellm", - groupLabel: "LiteLLM", - groupHint: "Unified LLM gateway (100+ providers)", - }, { value: "custom-api-key", label: "Custom Provider", diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index aa992980140..e248c35dfae 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -39,6 +39,15 @@ describe("buildAuthChoiceOptions", () => { it("includes core and provider-specific auth choices", () => { resolveManifestProviderAuthChoices.mockReturnValue([ + { + pluginId: "chutes", + providerId: "chutes", + methodId: "oauth", + choiceId: "chutes", + choiceLabel: "Chutes (OAuth)", + groupId: "chutes", + groupLabel: "Chutes", + }, { pluginId: "github-copilot", providerId: "github-copilot", @@ -66,6 +75,15 @@ describe("buildAuthChoiceOptions", () => { groupId: "openai", groupLabel: "OpenAI", }, + { + pluginId: "litellm", + providerId: "litellm", + methodId: "api-key", + choiceId: "litellm-api-key", + choiceLabel: "LiteLLM API key", + groupId: "litellm", + groupLabel: "LiteLLM", + }, { pluginId: "moonshot", providerId: "moonshot", @@ -206,6 +224,20 @@ describe("buildAuthChoiceOptions", () => { it("builds cli help choices from the same catalog", () => { resolveManifestProviderAuthChoices.mockReturnValue([ + { + pluginId: "chutes", + providerId: "chutes", + methodId: "oauth", + choiceId: "chutes", + choiceLabel: "Chutes (OAuth)", + }, + { + pluginId: "litellm", + providerId: "litellm", + methodId: "api-key", + choiceId: "litellm-api-key", + choiceLabel: "LiteLLM API key", + }, { pluginId: "openai", providerId: "openai", @@ -277,11 +309,24 @@ describe("buildAuthChoiceOptions", () => { expect(cliChoices).not.toContain("ollama"); expect(cliChoices).not.toContain("openai-api-key"); + expect(cliChoices).not.toContain("chutes"); + expect(cliChoices).not.toContain("litellm-api-key"); + expect(cliChoices).toContain("custom-api-key"); expect(cliChoices).toContain("skip"); }); it("shows Chutes in grouped provider selection", () => { - resolveManifestProviderAuthChoices.mockReturnValue([]); + resolveManifestProviderAuthChoices.mockReturnValue([ + { + pluginId: "chutes", + providerId: "chutes", + methodId: "oauth", + choiceId: "chutes", + choiceLabel: "Chutes (OAuth)", + groupId: "chutes", + groupLabel: "Chutes", + }, + ]); const { groups } = buildAuthChoiceGroups({ store: EMPTY_STORE, includeSkip: false, @@ -292,6 +337,28 @@ describe("buildAuthChoiceOptions", () => { expect(chutesGroup?.options.some((opt) => opt.value === "chutes")).toBe(true); }); + it("shows LiteLLM in grouped provider selection", () => { + resolveManifestProviderAuthChoices.mockReturnValue([ + { + pluginId: "litellm", + providerId: "litellm", + methodId: "api-key", + choiceId: "litellm-api-key", + choiceLabel: "LiteLLM API key", + groupId: "litellm", + groupLabel: "LiteLLM", + }, + ]); + const { groups } = buildAuthChoiceGroups({ + store: EMPTY_STORE, + includeSkip: false, + }); + const litellmGroup = groups.find((group) => group.value === "litellm"); + + expect(litellmGroup).toBeDefined(); + expect(litellmGroup?.options.some((opt) => opt.value === "litellm-api-key")).toBe(true); + }); + it("groups OpenCode Zen and Go under one OpenCode entry", () => { resolveManifestProviderAuthChoices.mockReturnValue([ { diff --git a/src/commands/auth-choice.apply.api-key-providers.ts b/src/commands/auth-choice.apply.api-key-providers.ts deleted file mode 100644 index 0d508ff687f..00000000000 --- a/src/commands/auth-choice.apply.api-key-providers.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js"; -import { applyAuthProfileConfig } from "../plugins/provider-auth-helpers.js"; -import { LITELLM_DEFAULT_MODEL_REF, setLitellmApiKey } from "../plugins/provider-auth-storage.js"; -import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key.js"; -import { ensureApiKeyFromOptionEnvOrPrompt } from "./auth-choice.apply-helpers.js"; -import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; -import { applyLitellmConfig, applyLitellmProviderConfig } from "./onboard-auth.config-litellm.js"; -import type { SecretInputMode } from "./onboard-types.js"; - -type ApiKeyProviderConfigApplier = ( - config: ApplyAuthChoiceParams["config"], -) => ApplyAuthChoiceParams["config"]; - -type ApplyProviderDefaultModel = (args: { - defaultModel: string; - applyDefaultConfig: ApiKeyProviderConfigApplier; - applyProviderConfig: ApiKeyProviderConfigApplier; - noteDefault?: string; -}) => Promise; - -type ApplyApiKeyProviderParams = { - params: ApplyAuthChoiceParams; - authChoice: string; - config: ApplyAuthChoiceParams["config"]; - setConfig: (config: ApplyAuthChoiceParams["config"]) => void; - getConfig: () => ApplyAuthChoiceParams["config"]; - normalizedTokenProvider?: string; - requestedSecretInputMode?: SecretInputMode; - applyProviderDefaultModel: ApplyProviderDefaultModel; - getAgentModelOverride: () => string | undefined; -}; - -export async function applyLiteLlmApiKeyProvider({ - params, - authChoice, - config, - setConfig, - getConfig, - normalizedTokenProvider, - requestedSecretInputMode, - applyProviderDefaultModel, - getAgentModelOverride, -}: ApplyApiKeyProviderParams): Promise { - if (authChoice !== "litellm-api-key") { - return null; - } - - let nextConfig = config; - const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }); - const profileOrder = resolveAuthProfileOrder({ cfg: nextConfig, store, provider: "litellm" }); - const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId])); - const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined; - let profileId = "litellm:default"; - let hasCredential = Boolean(existingProfileId && existingCred?.type === "api_key"); - if (hasCredential && existingProfileId) { - profileId = existingProfileId; - } - - if (!hasCredential) { - await ensureApiKeyFromOptionEnvOrPrompt({ - token: params.opts?.token, - tokenProvider: normalizedTokenProvider, - secretInputMode: requestedSecretInputMode, - config: nextConfig, - expectedProviders: ["litellm"], - provider: "litellm", - envLabel: "LITELLM_API_KEY", - promptMessage: "Enter LiteLLM API key", - normalize: normalizeApiKeyInput, - validate: validateApiKeyInput, - prompter: params.prompter, - setCredential: async (apiKey, mode) => - setLitellmApiKey(apiKey, params.agentDir, { secretInputMode: mode }), - noteMessage: - "LiteLLM provides a unified API to 100+ LLM providers.\nGet your API key from your LiteLLM proxy or https://litellm.ai\nDefault proxy runs on http://localhost:4000", - noteTitle: "LiteLLM", - }); - hasCredential = true; - } - - if (hasCredential) { - nextConfig = applyAuthProfileConfig(nextConfig, { - profileId, - provider: "litellm", - mode: "api_key", - }); - } - setConfig(nextConfig); - await applyProviderDefaultModel({ - defaultModel: LITELLM_DEFAULT_MODEL_REF, - applyDefaultConfig: applyLitellmConfig, - applyProviderConfig: applyLitellmProviderConfig, - noteDefault: LITELLM_DEFAULT_MODEL_REF, - }); - return { config: getConfig(), agentModelOverride: getAgentModelOverride() }; -} diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 6376dd51c7d..787cae8ba5f 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -1,17 +1,8 @@ import { resolveManifestProviderApiKeyChoice } from "../plugins/provider-auth-choices.js"; -import { - createAuthChoiceDefaultModelApplierForMutableState, - normalizeSecretInputModeInput, - normalizeTokenProviderInput, -} from "./auth-choice.apply-helpers.js"; -import { applyLiteLlmApiKeyProvider } from "./auth-choice.apply.api-key-providers.js"; +import { normalizeTokenProviderInput } from "./auth-choice.apply-helpers.js"; import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import type { AuthChoice } from "./onboard-types.js"; -const CORE_API_KEY_TOKEN_PROVIDER_AUTH_CHOICES: Partial> = { - litellm: "litellm-api-key", -}; - export function normalizeApiKeyTokenProviderAuthChoice(params: { authChoice: AuthChoice; tokenProvider?: string; @@ -32,48 +23,12 @@ export function normalizeApiKeyTokenProviderAuthChoice(params: { config: params.config, workspaceDir: params.workspaceDir, env: params.env, - })?.choiceId as AuthChoice | undefined) ?? - CORE_API_KEY_TOKEN_PROVIDER_AUTH_CHOICES[normalizedTokenProvider] ?? - params.authChoice + })?.choiceId as AuthChoice | undefined) ?? params.authChoice ); } export async function applyAuthChoiceApiProviders( - params: ApplyAuthChoiceParams, + _params: ApplyAuthChoiceParams, ): Promise { - let nextConfig = params.config; - let agentModelOverride: string | undefined; - const applyProviderDefaultModel = createAuthChoiceDefaultModelApplierForMutableState( - params, - () => nextConfig, - (config) => (nextConfig = config), - () => agentModelOverride, - (model) => (agentModelOverride = model), - ); - - const authChoice = normalizeApiKeyTokenProviderAuthChoice({ - authChoice: params.authChoice, - tokenProvider: params.opts?.tokenProvider, - config: params.config, - env: process.env, - }); - const normalizedTokenProvider = normalizeTokenProviderInput(params.opts?.tokenProvider); - const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode); - - const litellmResult = await applyLiteLlmApiKeyProvider({ - params, - authChoice, - config: nextConfig, - setConfig: (config) => (nextConfig = config), - getConfig: () => nextConfig, - normalizedTokenProvider, - requestedSecretInputMode, - applyProviderDefaultModel, - getAgentModelOverride: () => agentModelOverride, - }); - if (litellmResult) { - return litellmResult; - } - return null; } diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index d80560ac663..05c540cd33b 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -8,6 +8,7 @@ import googlePlugin from "../../extensions/google/index.js"; import huggingfacePlugin from "../../extensions/huggingface/index.js"; import kimiCodingPlugin from "../../extensions/kimi-coding/index.js"; import minimaxPlugin from "../../extensions/minimax/index.js"; +import litellmPlugin from "../../extensions/litellm/index.js"; import mistralPlugin from "../../extensions/mistral/index.js"; import moonshotPlugin from "../../extensions/moonshot/index.js"; import ollamaPlugin from "../../extensions/ollama/index.js"; @@ -94,6 +95,7 @@ function createDefaultProviderPlugins() { googlePlugin, huggingfacePlugin, kimiCodingPlugin, + litellmPlugin, minimaxPlugin, mistralPlugin, moonshotPlugin, diff --git a/src/commands/onboard-auth.config-litellm.ts b/src/commands/onboard-auth.config-litellm.ts deleted file mode 100644 index 2dd60bab894..00000000000 --- a/src/commands/onboard-auth.config-litellm.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { OpenClawConfig } from "../config/config.js"; -import { LITELLM_DEFAULT_MODEL_REF } from "../plugins/provider-auth-storage.js"; -import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithDefaultModel, -} from "../plugins/provider-onboarding-config.js"; - -export const LITELLM_BASE_URL = "http://localhost:4000"; -export const LITELLM_DEFAULT_MODEL_ID = "claude-opus-4-6"; -const LITELLM_DEFAULT_CONTEXT_WINDOW = 128_000; -const LITELLM_DEFAULT_MAX_TOKENS = 8_192; -const LITELLM_DEFAULT_COST = { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, -}; - -function buildLitellmModelDefinition(): { - id: string; - name: string; - reasoning: boolean; - input: Array<"text" | "image">; - cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; - contextWindow: number; - maxTokens: number; -} { - return { - id: LITELLM_DEFAULT_MODEL_ID, - name: "Claude Opus 4.6", - reasoning: true, - input: ["text", "image"], - cost: LITELLM_DEFAULT_COST, - contextWindow: LITELLM_DEFAULT_CONTEXT_WINDOW, - maxTokens: LITELLM_DEFAULT_MAX_TOKENS, - }; -} - -export function applyLitellmProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[LITELLM_DEFAULT_MODEL_REF] = { - ...models[LITELLM_DEFAULT_MODEL_REF], - alias: models[LITELLM_DEFAULT_MODEL_REF]?.alias ?? "LiteLLM", - }; - - const defaultModel = buildLitellmModelDefinition(); - - const existingProvider = cfg.models?.providers?.litellm as { baseUrl?: unknown } | undefined; - const resolvedBaseUrl = - typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : ""; - - return applyProviderConfigWithDefaultModel(cfg, { - agentModels: models, - providerId: "litellm", - api: "openai-completions", - baseUrl: resolvedBaseUrl || LITELLM_BASE_URL, - defaultModel, - defaultModelId: LITELLM_DEFAULT_MODEL_ID, - }); -} - -export function applyLitellmConfig(cfg: OpenClawConfig): OpenClawConfig { - const next = applyLitellmProviderConfig(cfg); - return applyAgentDefaultModelPrimary(next, LITELLM_DEFAULT_MODEL_REF); -} diff --git a/src/commands/onboard-auth.test.ts b/src/commands/onboard-auth.test.ts index 0cbbca2276c..e59f3caa870 100644 --- a/src/commands/onboard-auth.test.ts +++ b/src/commands/onboard-auth.test.ts @@ -36,6 +36,7 @@ import { } from "../../extensions/xai/onboard.js"; import { applyXiaomiConfig, applyXiaomiProviderConfig } from "../../extensions/xiaomi/onboard.js"; import { applyZaiConfig, applyZaiProviderConfig } from "../../extensions/zai/onboard.js"; +import { applyLitellmProviderConfig } from "../../extensions/litellm/onboard.js"; import { SYNTHETIC_DEFAULT_MODEL_ID } from "../agents/synthetic-models.js"; import type { OpenClawConfig } from "../config/config.js"; import { @@ -55,7 +56,6 @@ import { ZAI_CODING_CN_BASE_URL, ZAI_GLOBAL_BASE_URL, } from "../plugins/provider-model-definitions.js"; -import { applyLitellmProviderConfig } from "./onboard-auth.config-litellm.js"; import { createAuthTestLifecycle, readAuthProfilesForAgent, diff --git a/src/commands/onboard-core-auth-flags.ts b/src/commands/onboard-core-auth-flags.ts index 38663dbbccb..6b1c865de69 100644 --- a/src/commands/onboard-core-auth-flags.ts +++ b/src/commands/onboard-core-auth-flags.ts @@ -10,12 +10,4 @@ export type OnboardCoreAuthFlag = { description: string; }; -export const CORE_ONBOARD_AUTH_FLAGS: ReadonlyArray = [ - { - optionKey: "litellmApiKey", - authChoice: "litellm-api-key", - cliFlag: "--litellm-api-key", - cliOption: "--litellm-api-key ", - description: "LiteLLM API key", - }, -]; +export const CORE_ONBOARD_AUTH_FLAGS: ReadonlyArray = []; diff --git a/src/commands/onboard-non-interactive.provider-auth.test.ts b/src/commands/onboard-non-interactive.provider-auth.test.ts index 83318be10c0..266b148e818 100644 --- a/src/commands/onboard-non-interactive.provider-auth.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.test.ts @@ -37,6 +37,7 @@ vi.mock("./onboard-non-interactive/local/auth-choice.plugin-providers.js", async { default: volcenginePlugin }, { default: byteplusPlugin }, { default: anthropicPlugin }, + { default: litellmPlugin }, { default: mistralPlugin }, { default: togetherPlugin }, { default: qianfanPlugin }, @@ -57,6 +58,7 @@ vi.mock("./onboard-non-interactive/local/auth-choice.plugin-providers.js", async import("../../extensions/volcengine/index.ts"), import("../../extensions/byteplus/index.ts"), import("../../extensions/anthropic/index.ts"), + import("../../extensions/litellm/index.ts"), import("../../extensions/mistral/index.ts"), import("../../extensions/together/index.ts"), import("../../extensions/qianfan/index.ts"), @@ -100,6 +102,7 @@ vi.mock("./onboard-non-interactive/local/auth-choice.plugin-providers.js", async volcenginePlugin, byteplusPlugin, anthropicPlugin, + litellmPlugin, mistralPlugin, togetherPlugin, qianfanPlugin, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts b/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts deleted file mode 100644 index bb9ab999411..00000000000 --- a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { OpenClawConfig } from "../../../config/config.js"; -import type { SecretInput } from "../../../config/types.secrets.js"; -import { applyAuthProfileConfig } from "../../../plugins/provider-auth-helpers.js"; -import { setLitellmApiKey } from "../../../plugins/provider-auth-storage.js"; -import type { RuntimeEnv } from "../../../runtime.js"; -import { applyLitellmConfig } from "../../onboard-auth.config-litellm.js"; -import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; - -type ApiKeyStorageOptions = { - secretInputMode: "plaintext" | "ref"; -}; - -type ResolvedNonInteractiveApiKey = { - key: string; - source: "profile" | "env" | "flag"; -}; - -export async function applySimpleNonInteractiveApiKeyChoice(params: { - authChoice: AuthChoice; - nextConfig: OpenClawConfig; - baseConfig: OpenClawConfig; - opts: OnboardOptions; - runtime: RuntimeEnv; - apiKeyStorageOptions?: ApiKeyStorageOptions; - resolveApiKey: (input: { - provider: string; - cfg: OpenClawConfig; - flagValue?: string; - flagName: `--${string}`; - envVar: string; - runtime: RuntimeEnv; - }) => Promise; - maybeSetResolvedApiKey: ( - resolved: ResolvedNonInteractiveApiKey, - setter: (value: SecretInput) => Promise | void, - ) => Promise; -}): Promise { - if (params.authChoice !== "litellm-api-key") { - return undefined; - } - - const resolved = await params.resolveApiKey({ - provider: "litellm", - cfg: params.baseConfig, - flagValue: params.opts.litellmApiKey, - flagName: "--litellm-api-key", - envVar: "LITELLM_API_KEY", - runtime: params.runtime, - }); - if (!resolved) { - return null; - } - if ( - !(await params.maybeSetResolvedApiKey(resolved, (value) => - setLitellmApiKey(value, undefined, params.apiKeyStorageOptions), - )) - ) { - return null; - } - return applyLitellmConfig( - applyAuthProfileConfig(params.nextConfig, { - profileId: "litellm:default", - provider: "litellm", - mode: "api_key", - }), - ); -} diff --git a/src/commands/onboard-non-interactive/local/auth-choice.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.test.ts index b3255e7b4bb..e3ad83a24bf 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.test.ts @@ -2,13 +2,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; import { applyNonInteractiveAuthChoice } from "./auth-choice.js"; -const applySimpleNonInteractiveApiKeyChoice = vi.hoisted(() => - vi.fn<() => Promise>(async () => undefined), -); -vi.mock("./auth-choice.api-key-providers.js", () => ({ - applySimpleNonInteractiveApiKeyChoice, -})); - const applyNonInteractivePluginProviderChoice = vi.hoisted(() => vi.fn(async () => undefined)); vi.mock("./auth-choice.plugin-providers.js", () => ({ applyNonInteractivePluginProviderChoice, @@ -32,7 +25,7 @@ function createRuntime() { } describe("applyNonInteractiveAuthChoice", () => { - it("resolves plugin provider auth before builtin API key fallbacks", async () => { + it("resolves plugin provider auth before builtin custom-provider handling", async () => { const runtime = createRuntime(); const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; const resolvedConfig = { auth: { profiles: { "openai:default": { mode: "api_key" } } } }; @@ -48,6 +41,5 @@ describe("applyNonInteractiveAuthChoice", () => { expect(result).toBe(resolvedConfig); expect(applyNonInteractivePluginProviderChoice).toHaveBeenCalledOnce(); - expect(applySimpleNonInteractiveApiKeyChoice).not.toHaveBeenCalled(); }); }); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index e9119dcd466..6ffeb1b1a68 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -20,7 +20,6 @@ import { } from "../../onboard-custom.js"; import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; import { resolveNonInteractiveApiKey } from "../api-keys.js"; -import { applySimpleNonInteractiveApiKeyChoice } from "./auth-choice.api-key-providers.js"; import { applyNonInteractivePluginProviderChoice } from "./auth-choice.plugin-providers.js"; type ResolvedNonInteractiveApiKey = NonNullable< @@ -172,20 +171,6 @@ export async function applyNonInteractiveAuthChoice(params: { return pluginProviderChoice; } - const simpleApiKeyChoice = await applySimpleNonInteractiveApiKeyChoice({ - authChoice, - nextConfig, - baseConfig, - opts, - runtime, - apiKeyStorageOptions, - resolveApiKey, - maybeSetResolvedApiKey, - }); - if (simpleApiKeyChoice !== undefined) { - return simpleApiKeyChoice; - } - if (authChoice === "cloudflare-ai-gateway-api-key") { const accountId = opts.cloudflareAiGatewayAccountId?.trim() ?? ""; const gatewayId = opts.cloudflareAiGatewayGatewayId?.trim() ?? ""; diff --git a/src/generated/bundled-plugin-entries.generated.ts b/src/generated/bundled-plugin-entries.generated.ts index 745d051e6e4..34737f83082 100644 --- a/src/generated/bundled-plugin-entries.generated.ts +++ b/src/generated/bundled-plugin-entries.generated.ts @@ -33,6 +33,7 @@ export async function loadGeneratedBundledPluginEntries() { kilocodePluginModule, kimiCodingPluginModule, linePluginModule, + litellmPluginModule, llmTaskPluginModule, lobsterPluginModule, matrixPluginModule, @@ -111,6 +112,7 @@ export async function loadGeneratedBundledPluginEntries() { import("../../extensions/kilocode/index.js"), import("../../extensions/kimi-coding/index.js"), import("../../extensions/line/index.js"), + import("../../extensions/litellm/index.js"), import("../../extensions/llm-task/index.js"), import("../../extensions/lobster/index.js"), import("../../extensions/matrix/index.js"), @@ -190,6 +192,7 @@ export async function loadGeneratedBundledPluginEntries() { kilocodePluginModule.default, kimiCodingPluginModule.default, linePluginModule.default, + litellmPluginModule.default, llmTaskPluginModule.default, lobsterPluginModule.default, matrixPluginModule.default, diff --git a/src/plugins/bundled-plugin-metadata.generated.ts b/src/plugins/bundled-plugin-metadata.generated.ts index 887c7fffc1e..eecc6addcb6 100644 --- a/src/plugins/bundled-plugin-metadata.generated.ts +++ b/src/plugins/bundled-plugin-metadata.generated.ts @@ -8010,6 +8010,48 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, }, }, + { + dirName: "litellm", + idHint: "litellm", + source: { + source: "./index.ts", + built: "index.js", + }, + packageName: "@openclaw/litellm-provider", + packageVersion: "2026.3.26", + packageDescription: "OpenClaw LiteLLM provider plugin", + packageManifest: { + extensions: ["./index.ts"], + }, + manifest: { + id: "litellm", + configSchema: { + type: "object", + additionalProperties: false, + properties: {}, + }, + providers: ["litellm"], + providerAuthEnvVars: { + litellm: ["LITELLM_API_KEY"], + }, + providerAuthChoices: [ + { + provider: "litellm", + method: "api-key", + choiceId: "litellm-api-key", + choiceLabel: "LiteLLM API key", + choiceHint: "Unified gateway for 100+ LLM providers", + groupId: "litellm", + groupLabel: "LiteLLM", + groupHint: "Unified LLM gateway (100+ providers)", + optionKey: "litellmApiKey", + cliFlag: "--litellm-api-key", + cliOption: "--litellm-api-key ", + cliDescription: "LiteLLM API key", + }, + ], + }, + }, { dirName: "llm-task", idHint: "llm-task", diff --git a/src/plugins/bundled-provider-auth-env-vars.generated.ts b/src/plugins/bundled-provider-auth-env-vars.generated.ts index 3af9f191601..041de85b61e 100644 --- a/src/plugins/bundled-provider-auth-env-vars.generated.ts +++ b/src/plugins/bundled-provider-auth-env-vars.generated.ts @@ -16,6 +16,7 @@ export const BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES = { kilocode: ["KILOCODE_API_KEY"], kimi: ["KIMI_API_KEY", "KIMICODE_API_KEY"], "kimi-coding": ["KIMI_API_KEY", "KIMICODE_API_KEY"], + litellm: ["LITELLM_API_KEY"], "microsoft-foundry": ["AZURE_OPENAI_API_KEY"], minimax: ["MINIMAX_API_KEY"], "minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"], diff --git a/src/plugins/provider-auth-choice-preference.ts b/src/plugins/provider-auth-choice-preference.ts index f2883731656..7c3e51048ed 100644 --- a/src/plugins/provider-auth-choice-preference.ts +++ b/src/plugins/provider-auth-choice-preference.ts @@ -3,8 +3,6 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveManifestProviderAuthChoice } from "./provider-auth-choices.js"; const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { - chutes: "chutes", - "litellm-api-key": "litellm", "custom-api-key": "custom", };