diff --git a/apps/cli/README.md b/apps/cli/README.md index d4405364405..9c86dd7ed46 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -156,12 +156,13 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo | `-x, --exit-on-complete` | Exit the process when task completes (useful for testing) | `false` | | `-y, --yes` | Non-interactive mode: auto-approve all actions | `false` | | `-k, --api-key ` | API key for the LLM provider | From env var | -| `-p, --provider ` | API provider (anthropic, openai, openrouter, etc.) | `openrouter` | +| `-p, --provider ` | API provider (anthropic, openai, openrouter, litellm, etc.) | `openrouter` | | `-m, --model ` | Model to use | `anthropic/claude-sonnet-4.5` | | `-M, --mode ` | Mode to start in (code, architect, ask, debug, etc.) | `code` | | `-r, --reasoning-effort ` | Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh) | `medium` | | `--ephemeral` | Run without persisting state (uses temporary storage) | `false` | | `--no-tui` | Disable TUI, use plain text output | `false` | +| `--litellm-base-url ` | Base URL for LiteLLM endpoint | `http://localhost:4000` | ## Auth Commands @@ -181,8 +182,16 @@ The CLI will look for API keys in environment variables if not provided via `--a | openai | `OPENAI_API_KEY` | | openrouter | `OPENROUTER_API_KEY` | | google/gemini | `GOOGLE_API_KEY` | +| litellm | `LITELLM_API_KEY` | | ... | ... | +**LiteLLM Environment Variables:** + +| Variable | Description | +| ------------------ | ---------------------------------------------------------------- | +| `LITELLM_API_KEY` | API key for LiteLLM | +| `LITELLM_BASE_URL` | Base URL for LiteLLM endpoint (default: `http://localhost:4000`) | + **Authentication Environment Variables:** | Variable | Description | diff --git a/apps/cli/src/agent/extension-host.ts b/apps/cli/src/agent/extension-host.ts index e1f55a30d1f..c3abbbf5ac3 100644 --- a/apps/cli/src/agent/extension-host.ts +++ b/apps/cli/src/agent/extension-host.ts @@ -74,6 +74,10 @@ export interface ExtensionHostOptions { * running in an integration test and we want to see the output. */ integrationTest?: boolean + /** + * Base URL for LiteLLM endpoint (only used when provider is "litellm"). + */ + litellmBaseUrl?: string } interface ExtensionModule { @@ -191,7 +195,12 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac commandExecutionTimeout: 30, browserToolEnabled: false, enableCheckpoints: false, - ...getProviderSettings(this.options.provider, this.options.apiKey, this.options.model), + ...getProviderSettings( + this.options.provider, + this.options.apiKey, + this.options.model, + this.options.litellmBaseUrl, + ), } this.initialSettings = this.options.nonInteractive diff --git a/apps/cli/src/commands/cli/run.ts b/apps/cli/src/commands/cli/run.ts index 663ed5cf750..4e7c961f67e 100644 --- a/apps/cli/src/commands/cli/run.ts +++ b/apps/cli/src/commands/cli/run.ts @@ -69,6 +69,12 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption flagOptions.yes || flagOptions.dangerouslySkipPermissions || settings.dangerouslySkipPermissions || false const effectiveExitOnComplete = flagOptions.print || flagOptions.oneshot || settings.oneshot || false + // Determine effective LiteLLM base URL: CLI flag > env var > default (when using litellm provider) + let effectiveLitellmBaseUrl: string | undefined = flagOptions.litellmBaseUrl || process.env.LITELLM_BASE_URL + if (effectiveProvider === "litellm" && !effectiveLitellmBaseUrl) { + effectiveLitellmBaseUrl = "http://localhost:4000" + } + const extensionHostOptions: ExtensionHostOptions = { mode: effectiveMode, reasoningEffort: effectiveReasoningEffort === "unspecified" ? undefined : effectiveReasoningEffort, @@ -81,6 +87,7 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption ephemeral: flagOptions.ephemeral, debug: flagOptions.debug, exitOnComplete: effectiveExitOnComplete, + litellmBaseUrl: effectiveLitellmBaseUrl, } // Roo Code Cloud Authentication diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 5b663c2bdcd..b37a2663f38 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -35,6 +35,10 @@ program 'Output format (only works with --print): "text" (default), "json" (single result), or "stream-json" (realtime streaming)', "text", ) + .option( + "--litellm-base-url ", + "Base URL for LiteLLM endpoint (default: http://localhost:4000 when using litellm provider)", + ) .action(run) const authCommand = program.command("auth").description("Manage authentication for Roo Code Cloud") diff --git a/apps/cli/src/lib/utils/__tests__/provider.test.ts b/apps/cli/src/lib/utils/__tests__/provider.test.ts index 70d8a2a5557..a1b4b1cc2f3 100644 --- a/apps/cli/src/lib/utils/__tests__/provider.test.ts +++ b/apps/cli/src/lib/utils/__tests__/provider.test.ts @@ -1,4 +1,4 @@ -import { getApiKeyFromEnv } from "../provider.js" +import { getApiKeyFromEnv, getProviderSettings } from "../provider.js" describe("getApiKeyFromEnv", () => { const originalEnv = process.env @@ -27,8 +27,51 @@ describe("getApiKeyFromEnv", () => { expect(getApiKeyFromEnv("openai-native")).toBe("test-openai-key") }) + it("should return API key from environment variable for litellm", () => { + process.env.LITELLM_API_KEY = "test-litellm-key" + expect(getApiKeyFromEnv("litellm")).toBe("test-litellm-key") + }) + it("should return undefined when API key is not set", () => { delete process.env.ANTHROPIC_API_KEY expect(getApiKeyFromEnv("anthropic")).toBeUndefined() }) }) + +describe("getProviderSettings", () => { + it("should return LiteLLM settings with API key, model, and base URL", () => { + const settings = getProviderSettings("litellm", "test-api-key", "claude-3-sonnet", "http://localhost:4000") + expect(settings).toEqual({ + apiProvider: "litellm", + litellmApiKey: "test-api-key", + litellmModelId: "claude-3-sonnet", + litellmBaseUrl: "http://localhost:4000", + }) + }) + + it("should return LiteLLM settings without base URL when not provided", () => { + const settings = getProviderSettings("litellm", "test-api-key", "claude-3-sonnet") + expect(settings).toEqual({ + apiProvider: "litellm", + litellmApiKey: "test-api-key", + litellmModelId: "claude-3-sonnet", + }) + }) + + it("should return LiteLLM settings with only API key when model is not provided", () => { + const settings = getProviderSettings("litellm", "test-api-key", undefined) + expect(settings).toEqual({ + apiProvider: "litellm", + litellmApiKey: "test-api-key", + }) + }) + + it("should return anthropic settings correctly", () => { + const settings = getProviderSettings("anthropic", "test-api-key", "claude-3-opus") + expect(settings).toEqual({ + apiProvider: "anthropic", + apiKey: "test-api-key", + apiModelId: "claude-3-opus", + }) + }) +}) diff --git a/apps/cli/src/lib/utils/provider.ts b/apps/cli/src/lib/utils/provider.ts index 64aec430c1b..f0bcaf95cd1 100644 --- a/apps/cli/src/lib/utils/provider.ts +++ b/apps/cli/src/lib/utils/provider.ts @@ -9,6 +9,7 @@ const envVarMap: Record = { openrouter: "OPENROUTER_API_KEY", "vercel-ai-gateway": "VERCEL_AI_GATEWAY_API_KEY", roo: "ROO_API_KEY", + litellm: "LITELLM_API_KEY", } export function getEnvVarName(provider: SupportedProvider): string { @@ -24,6 +25,7 @@ export function getProviderSettings( provider: SupportedProvider, apiKey: string | undefined, model: string | undefined, + baseUrl?: string, ): RooCodeSettings { const config: RooCodeSettings = { apiProvider: provider } @@ -52,6 +54,11 @@ export function getProviderSettings( if (apiKey) config.rooApiKey = apiKey if (model) config.apiModelId = model break + case "litellm": + if (apiKey) config.litellmApiKey = apiKey + if (model) config.litellmModelId = model + if (baseUrl) config.litellmBaseUrl = baseUrl + break default: if (apiKey) config.apiKey = apiKey if (model) config.apiModelId = model diff --git a/apps/cli/src/types/types.ts b/apps/cli/src/types/types.ts index 05392ccca86..7429bbc9b09 100644 --- a/apps/cli/src/types/types.ts +++ b/apps/cli/src/types/types.ts @@ -8,6 +8,7 @@ export const supportedProviders = [ "openrouter", "vercel-ai-gateway", "roo", + "litellm", ] as const satisfies ProviderName[] export type SupportedProvider = (typeof supportedProviders)[number] @@ -34,6 +35,7 @@ export type FlagOptions = { ephemeral: boolean oneshot: boolean outputFormat?: OutputFormat + litellmBaseUrl?: string } export enum OnboardingProviderChoice {