Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ export CUSTOM_LLM_TOP_P=1 # Top P (default: 1)

When these variables are set, Easy LLM CLI will use your custom LLM instead of the default Gemini model.

For Atlas Cloud, you can use the OpenAI-compatible endpoint with fewer variables:

```bash
export ATLASCLOUD_API_KEY="your-atlascloud-api-key"
# Optional: export CUSTOM_LLM_MODEL_NAME="deepseek-ai/deepseek-v4-pro"
```

By default, Atlas Cloud uses `https://api.atlascloud.ai/v1` with `qwen/qwen3.5-flash`.


## Examples

Expand Down Expand Up @@ -191,4 +200,3 @@ Use MCP servers to integrate your local system tools with your enterprise collab
```text
> Organize my PDF invoices by month of expenditure.
```

11 changes: 11 additions & 0 deletions docs/cli/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia
- It automatically uses the credentials of the logged-in user in the Cloud Shell environment.
- This is the default authentication method when running in Cloud Shell and no other method is configured.

5. **OpenAI-compatible custom LLM API:**
- Set `USE_CUSTOM_LLM=true`, `CUSTOM_LLM_ENDPOINT`, and `CUSTOM_LLM_MODEL_NAME` for any OpenAI-compatible provider.
- Atlas Cloud can be enabled with only `ATLASCLOUD_API_KEY`; the CLI will use `https://api.atlascloud.ai/v1` and the `qwen/qwen3.5-flash` model by default.
- You can still override the Atlas Cloud default model with `CUSTOM_LLM_MODEL_NAME` when needed.

```bash
export ATLASCLOUD_API_KEY="YOUR_ATLASCLOUD_API_KEY"
# Optional override:
export CUSTOM_LLM_MODEL_NAME="deepseek-ai/deepseek-v4-pro"
```

### Persisting Environment Variables with `.env` Files

You can create a **`.gemini/.env`** file in your project directory or in your home directory. Creating a plain **`.env`** file also works, but `.gemini/.env` is recommended to keep Gemini variables isolated from other tools.
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/config/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,32 @@ describe('validateAuthMethod', () => {
});
});

describe('CUSTOM_LLM_API', () => {
it('should return null if ATLASCLOUD_API_KEY is set', () => {
process.env.ATLASCLOUD_API_KEY = 'test-atlas-key';
expect(validateAuthMethod(AuthType.CUSTOM_LLM_API)).toBeNull();
});

it('should return null if CUSTOM_LLM_ENDPOINT and CUSTOM_LLM_MODEL_NAME are set', () => {
process.env.CUSTOM_LLM_ENDPOINT = 'https://example.test/v1';
process.env.CUSTOM_LLM_MODEL_NAME = 'example-model';
expect(validateAuthMethod(AuthType.CUSTOM_LLM_API)).toBeNull();
});

it('should return an endpoint error if custom LLM variables are missing', () => {
expect(validateAuthMethod(AuthType.CUSTOM_LLM_API)).toBe(
'CUSTOM_LLM_ENDPOINT environment variable not found. Add that to your .env and try again, no reload needed! Alternatively set ATLASCLOUD_API_KEY to use the Atlas Cloud OpenAI-compatible endpoint.',
);
});

it('should return a model error if CUSTOM_LLM_MODEL_NAME is missing', () => {
process.env.CUSTOM_LLM_ENDPOINT = 'https://example.test/v1';
expect(validateAuthMethod(AuthType.CUSTOM_LLM_API)).toBe(
'CUSTOM_LLM_MODEL_NAME environment variable not found. Add that to your .env and try again, no reload needed! Alternatively set ATLASCLOUD_API_KEY to use the Atlas Cloud default model.',
);
});
});

it('should return an error message for an invalid auth method', () => {
expect(validateAuthMethod('invalid-method')).toBe(
'Invalid auth method selected.',
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/config/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,16 @@ export const validateAuthMethod = (authMethod: string): string | null => {
}

if (authMethod === AuthType.CUSTOM_LLM_API) {
if (process.env.ATLASCLOUD_API_KEY) {
return null;
}

if (!process.env.CUSTOM_LLM_ENDPOINT) {
return 'CUSTOM_LLM_ENDPOINT environment variable not found. Add that to your .env and try again, no reload needed!';
return 'CUSTOM_LLM_ENDPOINT environment variable not found. Add that to your .env and try again, no reload needed! Alternatively set ATLASCLOUD_API_KEY to use the Atlas Cloud OpenAI-compatible endpoint.';
}

if (!process.env.CUSTOM_LLM_MODEL_NAME) {
return 'CUSTOM_LLM_MODEL_NAME environment variable not found. Add that to your .env and try again, no reload needed!';
return 'CUSTOM_LLM_MODEL_NAME environment variable not found. Add that to your .env and try again, no reload needed! Alternatively set ATLASCLOUD_API_KEY to use the Atlas Cloud default model.';
}
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export async function main() {
}
}

if (process.env.USE_CUSTOM_LLM) {
if (process.env.USE_CUSTOM_LLM || process.env.ATLASCLOUD_API_KEY) {
settings.setValue(
SettingScope.User,
'selectedAuthType',
Expand Down
19 changes: 14 additions & 5 deletions packages/cli/src/ui/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
import React from 'react';
import { Box, Text } from 'ink';
import { Colors } from '../colors.js';
import { shortenPath, tildeifyPath, tokenLimit } from '@google/gemini-cli-core';
import {
resolveCustomLLMRuntimeConfig,
shortenPath,
tildeifyPath,
tokenLimit,
} from '@google/gemini-cli-core';
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
import process from 'node:process';
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
Expand Down Expand Up @@ -41,10 +46,14 @@ export const Footer: React.FC<FooterProps> = ({
const percentage = promptTokenCount / limit;

let modelInfo = model;
if (process.env.USE_CUSTOM_LLM && process.env.CUSTOM_LLM_MODEL_NAME) {
modelInfo =
process.env.CUSTOM_LLM_MODEL_NAME +
`(${process.env.CUSTOM_LLM_PROVIDER})`;
const customConfig = resolveCustomLLMRuntimeConfig();
if (
(process.env.USE_CUSTOM_LLM || process.env.ATLASCLOUD_API_KEY) &&
customConfig.modelName
) {
modelInfo = customConfig.provider
? `${customConfig.modelName}(${customConfig.provider})`
: customConfig.modelName;
}

return (
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/ui/hooks/useAuthCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
} from '@google/gemini-cli-core';

function isInitAuth(settings: LoadedSettings) {
if (process.env.USE_CUSTOM_LLM && process.env.USE_CUSTOM_LLM !== 'false') {
if (
(process.env.USE_CUSTOM_LLM && process.env.USE_CUSTOM_LLM !== 'false') ||
process.env.ATLASCLOUD_API_KEY
) {
return false;
}
if (settings.merged.selectedAuthType !== AuthType.CUSTOM_LLM_API) {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
DEFAULT_GEMINI_FLASH_MODEL,
} from './models.js';
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
import { resolveCustomLLMRuntimeConfig } from '../custom_llm/config.js';

export enum ApprovalMode {
DEFAULT = 'default',
Expand Down Expand Up @@ -278,8 +279,8 @@ export class Config {
}

getModel(): string {
if (process.env.USE_CUSTOM_LLM) {
return process.env.CUSTOM_LLM_MODEL_NAME || '';
if (process.env.USE_CUSTOM_LLM || process.env.ATLASCLOUD_API_KEY) {
return resolveCustomLLMRuntimeConfig().modelName;
}
return this.contentGeneratorConfig?.model || this.model;
}
Expand Down
60 changes: 60 additions & 0 deletions packages/core/src/custom_llm/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it } from 'vitest';
import {
ATLASCLOUD_DEFAULT_MODEL,
ATLASCLOUD_OPENAI_BASE_URL,
resolveCustomLLMRuntimeConfig,
} from './config.js';

describe('resolveCustomLLMRuntimeConfig', () => {
it('uses existing CUSTOM_LLM_* variables unchanged', () => {
const config = resolveCustomLLMRuntimeConfig({
CUSTOM_LLM_API_KEY: 'custom-key',
CUSTOM_LLM_ENDPOINT: 'https://example.test/v1',
CUSTOM_LLM_MODEL_NAME: 'custom-model',
CUSTOM_LLM_PROVIDER: 'custom-provider',
CUSTOM_LLM_TEMPERATURE: '0.3',
CUSTOM_LLM_MAX_TOKENS: '4096',
CUSTOM_LLM_TOP_P: '0.8',
});

expect(config).toEqual({
apiKey: 'custom-key',
baseURL: 'https://example.test/v1',
modelName: 'custom-model',
provider: 'custom-provider',
temperature: 0.3,
maxTokens: 4096,
topP: 0.8,
});
});

it('uses Atlas Cloud defaults when ATLASCLOUD_API_KEY is set', () => {
const config = resolveCustomLLMRuntimeConfig({
ATLASCLOUD_API_KEY: 'atlas-key',
});

expect(config.apiKey).toBe('atlas-key');
expect(config.baseURL).toBe(ATLASCLOUD_OPENAI_BASE_URL);
expect(config.modelName).toBe(ATLASCLOUD_DEFAULT_MODEL);
expect(config.provider).toBe('atlascloud');
});

it('lets CUSTOM_LLM_* override Atlas Cloud defaults', () => {
const config = resolveCustomLLMRuntimeConfig({
ATLASCLOUD_API_KEY: 'atlas-key',
CUSTOM_LLM_MODEL_NAME: 'deepseek-ai/deepseek-v4-pro',
CUSTOM_LLM_PROVIDER: 'atlascloud-eu',
});

expect(config.apiKey).toBe('atlas-key');
expect(config.baseURL).toBe(ATLASCLOUD_OPENAI_BASE_URL);
expect(config.modelName).toBe('deepseek-ai/deepseek-v4-pro');
expect(config.provider).toBe('atlascloud-eu');
});
});
41 changes: 41 additions & 0 deletions packages/core/src/custom_llm/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

export const ATLASCLOUD_OPENAI_BASE_URL = 'https://api.atlascloud.ai/v1';
export const ATLASCLOUD_DEFAULT_MODEL = 'qwen/qwen3.5-flash';
export const ATLASCLOUD_PROVIDER_NAME = 'atlascloud';

export interface CustomLLMRuntimeConfig {
apiKey: string;
baseURL: string;
modelName: string;
provider: string;
temperature: number;
maxTokens: number;
topP: number;
}

export function resolveCustomLLMRuntimeConfig(
env: NodeJS.ProcessEnv = process.env,
): CustomLLMRuntimeConfig {
const hasAtlasCloudKey = Boolean(env.ATLASCLOUD_API_KEY);

return {
apiKey: env.CUSTOM_LLM_API_KEY || env.ATLASCLOUD_API_KEY || '',
baseURL:
env.CUSTOM_LLM_ENDPOINT ||
(hasAtlasCloudKey ? ATLASCLOUD_OPENAI_BASE_URL : ''),
modelName:
env.CUSTOM_LLM_MODEL_NAME ||
(hasAtlasCloudKey ? ATLASCLOUD_DEFAULT_MODEL : ''),
provider:
env.CUSTOM_LLM_PROVIDER ||
(hasAtlasCloudKey ? ATLASCLOUD_PROVIDER_NAME : ''),
temperature: Number(env.CUSTOM_LLM_TEMPERATURE || 0),
maxTokens: Number(env.CUSTOM_LLM_MAX_TOKENS || 8192),
topP: Number(env.CUSTOM_LLM_TOP_P || 1),
};
}
20 changes: 8 additions & 12 deletions packages/core/src/custom_llm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,22 @@ import { ContentGenerator } from '../core/contentGenerator.js';
import { CustomLLMContentGeneratorConfig, ToolCallMap } from './types.js';
import { extractToolFunctions } from './util.js';
import { ModelConverter } from './converter.js';
import { resolveCustomLLMRuntimeConfig } from './config.js';

export class CustomLLMContentGenerator implements ContentGenerator {
private model: OpenAI;
private apiKey: string = process.env.CUSTOM_LLM_API_KEY || '';
private baseURL: string = process.env.CUSTOM_LLM_ENDPOINT || '';
private modelName: string = process.env.CUSTOM_LLM_MODEL_NAME || '';
private temperature: number = Number(process.env.CUSTOM_LLM_TEMPERATURE || 0);
private maxTokens: number = Number(process.env.CUSTOM_LLM_MAX_TOKENS || 8192);
private topP: number = Number(process.env.CUSTOM_LLM_TOP_P || 1);
private runtimeConfig = resolveCustomLLMRuntimeConfig();
private config: CustomLLMContentGeneratorConfig = {
model: this.modelName,
temperature: this.temperature,
max_tokens: this.maxTokens,
top_p: this.topP,
model: this.runtimeConfig.modelName,
temperature: this.runtimeConfig.temperature,
max_tokens: this.runtimeConfig.maxTokens,
top_p: this.runtimeConfig.topP,
};

constructor() {
this.model = new OpenAI({
apiKey: this.apiKey,
baseURL: this.baseURL,
apiKey: this.runtimeConfig.apiKey,
baseURL: this.runtimeConfig.baseURL,
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from './core/turn.js';
export * from './core/geminiRequest.js';
export * from './core/coreToolScheduler.js';
export * from './core/nonInteractiveToolExecutor.js';
export * from './custom_llm/config.js';

export * from './code_assist/codeAssist.js';
export * from './code_assist/oauth2.js';
Expand Down