diff --git a/docs/docs.json b/docs/docs.json index a463479aa..391509401 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1014,6 +1014,7 @@ "providers/vercel-ai-gateway", "providers/openrouter", "providers/synthetic", + "providers/litellm", "providers/opencode", "providers/glm", "providers/zai" diff --git a/docs/providers/litellm.md b/docs/providers/litellm.md new file mode 100644 index 000000000..2a8a7dc40 --- /dev/null +++ b/docs/providers/litellm.md @@ -0,0 +1,99 @@ +--- +summary: "Use LiteLLM as an OpenAI-compatible proxy in Clawdbot" +read_when: + - You want to use LiteLLM as a model provider + - You need to connect to a self-hosted LiteLLM proxy + - You want to use any model through an OpenAI-compatible API +--- +# LiteLLM + +LiteLLM is an OpenAI-compatible proxy that supports 100+ LLM APIs. Clawdbot +registers it as the `litellm` provider and uses the OpenAI Completions API. + +## Quick setup + +1) Set up your LiteLLM proxy (see [LiteLLM docs](https://docs.litellm.ai/)) +2) Set environment variables (optional): + - `LITELLM_API_KEY` - your LiteLLM API key + - `LITELLM_BASE_URL` - your LiteLLM endpoint (default: `http://localhost:4000`) + - `LITELLM_MODEL` - default model name (default: `gpt-4`) +3) Run onboarding: + +```bash +clawdbot onboard --auth-choice litellm-api-key +``` + +The wizard will prompt for: +- Base URL (your LiteLLM proxy endpoint) +- API key +- Model name (as configured in your LiteLLM proxy) + +## Config example + +```json5 +{ + env: { LITELLM_API_KEY: "sk-..." }, + agents: { + defaults: { + model: { primary: "litellm/gpt-4" }, + models: { "litellm/gpt-4": { alias: "GPT-4" } } + } + }, + models: { + mode: "merge", + providers: { + litellm: { + baseUrl: "http://localhost:4000", + apiKey: "${LITELLM_API_KEY}", + api: "openai-completions", + models: [ + { + id: "gpt-4", + name: "GPT-4", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192 + } + ] + } + } + } +} +``` + +## Multiple models + +Add additional models to your config as needed: + +```json5 +{ + models: { + providers: { + litellm: { + baseUrl: "http://localhost:4000", + apiKey: "${LITELLM_API_KEY}", + api: "openai-completions", + models: [ + { id: "gpt-4", name: "GPT-4", contextWindow: 128000, maxTokens: 8192 }, + { id: "claude-3-opus", name: "Claude Opus", contextWindow: 200000, maxTokens: 4096 }, + { id: "gemini-pro", name: "Gemini Pro", contextWindow: 32000, maxTokens: 8192 } + ] + } + } + } +} +``` + +Then switch models using: + +```bash +clawdbot config set agents.defaults.model.primary litellm/claude-3-opus +``` + +## Notes + +- Model refs use `litellm/` where `modelId` matches your LiteLLM config. +- The base URL should not include `/v1` - Clawdbot's OpenAI client appends it. +- Supported LiteLLM models depend on your proxy configuration. +- See [Model providers](/concepts/model-providers) for provider rules. diff --git a/src/agents/litellm-models.ts b/src/agents/litellm-models.ts new file mode 100644 index 000000000..fd45bd938 --- /dev/null +++ b/src/agents/litellm-models.ts @@ -0,0 +1,45 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +// LiteLLM is a proxy that supports many models, so the base URL and model +// are user-configurable. We provide sensible defaults for onboarding. +export const LITELLM_DEFAULT_BASE_URL = "http://localhost:4000"; +export const LITELLM_DEFAULT_MODEL_ID = "gpt-4"; +export const LITELLM_DEFAULT_MODEL_REF = `litellm/${LITELLM_DEFAULT_MODEL_ID}`; +export const LITELLM_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +export type LitellmModelEntry = { + id: string; + name: string; + reasoning?: boolean; + input?: readonly ("text" | "image")[]; + contextWindow?: number; + maxTokens?: number; +}; + +export function buildLitellmModelDefinition(entry: LitellmModelEntry): ModelDefinitionConfig { + return { + id: entry.id, + name: entry.name, + reasoning: entry.reasoning ?? false, + input: entry.input ? [...entry.input] : ["text"], + cost: LITELLM_DEFAULT_COST, + contextWindow: entry.contextWindow ?? 128000, + maxTokens: entry.maxTokens ?? 8192, + // LiteLLM proxies to various providers that may not support the OpenAI Responses API + // `store` parameter. Disable it by default to avoid "Extra inputs are not permitted" errors. + compat: { supportsStore: false }, + }; +} + +/** + * Creates a model reference for a LiteLLM model. + * The model ID can be any model supported by the LiteLLM proxy. + */ +export function litellmModelRef(modelId: string): string { + return `litellm/${modelId}`; +} diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 96e4e4ae6..e2d4d1fba 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -285,6 +285,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { venice: "VENICE_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", + litellm: "LITELLM_API_KEY", }; const envVar = envMap[normalized]; if (!envVar) return null; diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index 1792e6706..a58455b8f 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -77,17 +77,25 @@ export function resolveModel( } const providerCfg = providers[provider]; if (providerCfg || modelId.startsWith("mock-")) { + // Find the matching model definition from provider config to get compat settings + const modelDef = providerCfg?.models?.find((m) => m.id === modelId); const fallbackModel: Model = normalizeModelCompat({ id: modelId, - name: modelId, - api: providerCfg?.api ?? "openai-responses", + name: modelDef?.name ?? modelId, + api: modelDef?.api ?? providerCfg?.api ?? "openai-responses", provider, baseUrl: providerCfg?.baseUrl, - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: providerCfg?.models?.[0]?.contextWindow ?? DEFAULT_CONTEXT_TOKENS, - maxTokens: providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS, + reasoning: modelDef?.reasoning ?? false, + input: modelDef?.input ?? ["text"], + cost: modelDef?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: + modelDef?.contextWindow ?? + providerCfg?.models?.[0]?.contextWindow ?? + DEFAULT_CONTEXT_TOKENS, + maxTokens: + modelDef?.maxTokens ?? providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS, + // Preserve compat settings for provider-specific quirks (e.g., supportsStore for LiteLLM) + compat: modelDef?.compat, } as Model); return { model: fallbackModel, authStorage, modelRegistry }; } diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 8f31635f0..5a25fc29c 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|litellm-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", ) .option( "--token-provider ", @@ -76,6 +76,9 @@ export function registerOnboardCommand(program: Command) { .option("--synthetic-api-key ", "Synthetic API key") .option("--venice-api-key ", "Venice API key") .option("--opencode-zen-api-key ", "OpenCode Zen API key") + .option("--litellm-api-key ", "LiteLLM API key") + .option("--litellm-base-url ", "LiteLLM base URL (default: http://localhost:4000)") + .option("--litellm-model ", "LiteLLM model name") .option("--gateway-port ", "Gateway port") .option("--gateway-bind ", "Gateway bind: loopback|tailnet|lan|auto|custom") .option("--gateway-auth ", "Gateway auth: token|password") diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..9aa65f659 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -20,7 +20,8 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" - | "qwen"; + | "qwen" + | "litellm"; export type AuthChoiceGroup = { value: AuthChoiceGroupId; @@ -113,6 +114,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["opencode-zen"], }, + { + value: "litellm", + label: "LiteLLM", + hint: "OpenAI-compatible proxy (self-hosted)", + choices: ["litellm-api-key"], + }, ]; export function buildAuthChoiceOptions(params: { @@ -183,6 +190,11 @@ export function buildAuthChoiceOptions(params: { label: "MiniMax M2.1 Lightning", hint: "Faster, higher output cost", }); + options.push({ + value: "litellm-api-key", + label: "LiteLLM API key", + hint: "OpenAI-compatible proxy (any model)", + }); if (params.includeSkip) { options.push({ value: "skip", label: "Skip for now" }); } diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..c3d2c7364 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -15,6 +15,8 @@ import { applyAuthProfileConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, + applyLitellmConfig, + applyLitellmProviderConfig, applyMoonshotConfig, applyMoonshotProviderConfig, applyOpencodeZenConfig, @@ -36,6 +38,7 @@ import { VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, setGeminiApiKey, setKimiCodeApiKey, + setLitellmApiKey, setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, @@ -85,6 +88,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "venice-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; + } else if (params.opts.tokenProvider === "litellm") { + authChoice = "litellm-api-key"; } } @@ -579,5 +584,234 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "litellm-api-key") { + let hasCredential = false; + let apiKey: string | undefined; + + // Check for pre-provided credentials via CLI options + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "litellm") { + apiKey = normalizeApiKeyInput(params.opts.token); + await setLitellmApiKey(apiKey, params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "LiteLLM is an OpenAI-compatible proxy that supports many models.", + "You'll need to provide:", + " 1. Base URL (e.g., http://localhost:4000)", + " 2. API key", + " 3. Model selection (fetched from your LiteLLM instance)", + ].join("\n"), + "LiteLLM", + ); + } + + // Check for existing env key + const envKey = resolveEnvApiKey("litellm"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing LITELLM_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + apiKey = envKey.apiKey; + await setLitellmApiKey(apiKey, params.agentDir); + hasCredential = true; + } + } + + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter LiteLLM API key", + validate: validateApiKeyInput, + }); + apiKey = normalizeApiKeyInput(String(key)); + await setLitellmApiKey(apiKey, params.agentDir); + } + + // Prompt for base URL + const defaultBaseUrl = process.env.LITELLM_BASE_URL ?? "http://localhost:4000"; + const baseUrl = await params.prompter.text({ + message: "Enter LiteLLM base URL", + initialValue: defaultBaseUrl, + placeholder: defaultBaseUrl, + validate: (value) => { + if (!value?.trim()) return "Base URL is required"; + try { + new URL(value); + return undefined; + } catch { + return "Invalid URL format"; + } + }, + }); + + const normalizedBaseUrl = String(baseUrl).trim(); + + // Try to fetch available models from LiteLLM + type LitellmModelInfo = { id: string; maxInputTokens?: number; maxOutputTokens?: number }; + let availableModels: LitellmModelInfo[] = []; + const authHeaders: Record = apiKey ? { Authorization: `Bearer ${apiKey}` } : {}; + + // First fetch model list from /v1/models + try { + const modelsUrl = new URL("/v1/models", normalizedBaseUrl).toString(); + const response = await fetch(modelsUrl, { + headers: authHeaders, + signal: AbortSignal.timeout(10000), + }); + if (response.ok) { + const data = (await response.json()) as { + data?: Array<{ id: string }>; + }; + if (data.data && Array.isArray(data.data)) { + availableModels = data.data.map((m) => ({ id: m.id })); + } + } + } catch { + // Fetching models failed - will fall back to manual entry + } + + // Then fetch detailed model info from /model/info (LiteLLM-specific endpoint) + // This provides context window and max tokens info + type ModelInfoEntry = { + model_name: string; + model_info?: { + max_input_tokens?: number; + max_tokens?: number; + max_output_tokens?: number; + }; + }; + const modelInfoMap = new Map(); + try { + const modelInfoUrl = new URL("/model/info", normalizedBaseUrl).toString(); + const response = await fetch(modelInfoUrl, { + headers: authHeaders, + signal: AbortSignal.timeout(10000), + }); + if (response.ok) { + const data = (await response.json()) as { data?: ModelInfoEntry[] }; + if (data.data && Array.isArray(data.data)) { + for (const entry of data.data) { + if (entry.model_name && entry.model_info) { + modelInfoMap.set(entry.model_name, { + maxInputTokens: entry.model_info.max_input_tokens, + maxOutputTokens: entry.model_info.max_output_tokens ?? entry.model_info.max_tokens, + }); + } + } + } + } + } catch { + // Model info fetch failed - context window will need manual entry + } + + // Merge model info into available models + availableModels = availableModels.map((m) => { + const info = modelInfoMap.get(m.id); + return { + id: m.id, + maxInputTokens: info?.maxInputTokens, + maxOutputTokens: info?.maxOutputTokens, + }; + }); + + let normalizedModelId: string; + let contextWindow: number | undefined; + let maxTokens: number | undefined; + + if (availableModels.length > 0) { + // Let user select from available models + type SelectOption = { value: string; label: string; hint?: string }; + const modelOptions: SelectOption[] = availableModels.map((m) => ({ + value: m.id, + label: m.id, + hint: m.maxInputTokens ? `${Math.round(m.maxInputTokens / 1000)}k context` : undefined, + })); + modelOptions.push({ value: "__custom__", label: "Enter custom model name", hint: undefined }); + + const selectedModel = await params.prompter.select({ + message: `Select model (${availableModels.length} available)`, + options: modelOptions, + }); + + if (selectedModel === "__custom__") { + const customModel = await params.prompter.text({ + message: "Enter model name", + validate: (value) => (value?.trim() ? undefined : "Model name is required"), + }); + normalizedModelId = String(customModel).trim(); + } else { + normalizedModelId = String(selectedModel); + const modelInfo = availableModels.find((m) => m.id === normalizedModelId); + if (modelInfo?.maxInputTokens) { + contextWindow = modelInfo.maxInputTokens; + } + if (modelInfo?.maxOutputTokens) { + maxTokens = modelInfo.maxOutputTokens; + } + } + } else { + // Fall back to manual model entry + const defaultModel = process.env.LITELLM_MODEL ?? "gpt-4"; + const modelId = await params.prompter.text({ + message: "Enter model name (as configured in LiteLLM)", + initialValue: defaultModel, + placeholder: defaultModel, + validate: (value) => (value?.trim() ? undefined : "Model name is required"), + }); + normalizedModelId = String(modelId).trim(); + } + + // If context window wasn't auto-detected, prompt for it + if (!contextWindow) { + const contextInput = await params.prompter.text({ + message: "Enter context window size (tokens)", + initialValue: "128000", + placeholder: "128000", + validate: (value) => { + const num = Number(value); + if (Number.isNaN(num) || num <= 0) return "Must be a positive number"; + return undefined; + }, + }); + contextWindow = Number(contextInput); + } + + const modelRef = `litellm/${normalizedModelId}`; + + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "litellm:default", + provider: "litellm", + mode: "api_key", + }); + + if (params.setDefaultModel) { + nextConfig = applyLitellmConfig(nextConfig, { + baseUrl: normalizedBaseUrl, + modelId: normalizedModelId, + contextWindow, + maxTokens, + }); + await params.prompter.note( + `Default model set to ${modelRef}${contextWindow ? ` (${Math.round(contextWindow / 1000)}k context)` : ""}`, + "Model configured", + ); + } else { + nextConfig = applyLitellmProviderConfig(nextConfig, { + baseUrl: normalizedBaseUrl, + modelId: normalizedModelId, + contextWindow, + maxTokens, + }); + agentModelOverride = modelRef; + await noteAgentModel(modelRef); + } + + return { config: nextConfig, agentModelOverride }; + } + return null; } diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 921ee01d1..2b59d7aa1 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -411,6 +411,111 @@ export function applyVeniceConfig(cfg: MoltbotConfig): MoltbotConfig { }; } +/** + * Apply LiteLLM provider configuration without changing the default model. + * LiteLLM is a flexible proxy that supports many models, so base URL and model + * are user-configurable. + */ +export function applyLitellmProviderConfig( + cfg: ClawdbotConfig, + params: { + baseUrl: string; + modelId: string; + modelName?: string; + contextWindow?: number; + maxTokens?: number; + }, +): ClawdbotConfig { + const modelRef = `litellm/${params.modelId}`; + const models = { ...cfg.agents?.defaults?.models }; + models[modelRef] = { + ...models[modelRef], + alias: models[modelRef]?.alias ?? params.modelName ?? params.modelId, + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.litellm; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const newModel = { + id: params.modelId, + name: params.modelName ?? params.modelId, + reasoning: false, + input: ["text"] as ("text" | "image")[], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: params.contextWindow ?? 128000, + maxTokens: params.maxTokens ?? 8192, + // LiteLLM proxies to various providers that may not support the OpenAI Responses API + // `store` parameter. Disable it to avoid "Extra inputs are not permitted" errors. + compat: { supportsStore: false }, + }; + const hasModel = existingModels.some((model) => model.id === params.modelId); + const mergedModels = hasModel ? existingModels : [...existingModels, newModel]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.litellm = { + ...existingProviderRest, + baseUrl: params.baseUrl, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : [newModel], + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply LiteLLM provider configuration AND set LiteLLM as the default model. + * Use this when LiteLLM is the primary provider choice during onboarding. + */ +export function applyLitellmConfig( + cfg: ClawdbotConfig, + params: { + baseUrl: string; + modelId: string; + modelName?: string; + contextWindow?: number; + maxTokens?: number; + }, +): ClawdbotConfig { + const next = applyLitellmProviderConfig(cfg, params); + const modelRef = `litellm/${params.modelId}`; + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: modelRef, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: MoltbotConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index b2fb58542..b794daa7d 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -164,3 +164,17 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export const LITELLM_DEFAULT_MODEL_REF = "litellm/gpt-4"; + +export async function setLitellmApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "litellm:default", + credential: { + type: "api_key", + provider: "litellm", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..db1b2e614 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -7,6 +7,8 @@ export { applyAuthProfileConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, + applyLitellmConfig, + applyLitellmProviderConfig, applyMoonshotConfig, applyMoonshotProviderConfig, applyOpenrouterConfig, @@ -33,10 +35,12 @@ export { applyOpencodeZenProviderConfig, } from "./onboard-auth.config-opencode.js"; export { + LITELLM_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, setGeminiApiKey, setKimiCodeApiKey, + setLitellmApiKey, setMinimaxApiKey, setMoonshotApiKey, setOpencodeZenApiKey, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..d3b0c62dc 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -17,6 +17,7 @@ export type AuthChoice = | "kimi-code-api-key" | "synthetic-api-key" | "venice-api-key" + | "litellm-api-key" | "codex-cli" | "apiKey" | "gemini-api-key" @@ -71,6 +72,9 @@ export type OnboardOptions = { syntheticApiKey?: string; veniceApiKey?: string; opencodeZenApiKey?: string; + litellmApiKey?: string; + litellmBaseUrl?: string; + litellmModel?: string; gatewayPort?: number; gatewayBind?: GatewayBind; gatewayAuth?: GatewayAuthChoice; diff --git a/src/config/io.ts b/src/config/io.ts index 50f1edb82..b6f091fed 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -48,6 +48,7 @@ const SHELL_ENV_EXPECTED_KEYS = [ "AI_GATEWAY_API_KEY", "MINIMAX_API_KEY", "SYNTHETIC_API_KEY", + "LITELLM_API_KEY", "ELEVENLABS_API_KEY", "TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN",