diff --git a/README.md b/README.md index 7655b775a..8e788b420 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,7 @@ bun run build 2. **OpenAI / Gemini / Grok**: 对应各自协议的云端服务。 - **Gemini (Google Auth)**: 支持交互式浏览器登录。 1. 在 Google Cloud Console 的 **APIs & Services > OAuth consent screen** 中配置 OAuth 客户端(User Type 设为 External)。 - 2. 下载 credentials JSON 文件并保存到项目根目录的 `/.files/OAuth.json`。 - 3. 在 `/login` 配置界面中留空 API Key 并按 Enter,程序将自动拉起浏览器完成授权并自动拉取模型列表。 + 2. 下载 credentials JSON 文件并保存到项目根目录的 `.files/OAuth.json`。 3. 在 `/login` 配置界面中留空 API Key 并按 Enter,程序将自动拉起浏览器完成授权并自动拉取模型列表。 3. **Local LLM**: **(推荐)** 使用本地运行的模型。 - 支持 **Ollama**, **LM Studio**, **Jan.ai**, **LocalAI**。 - **Ollama 深度集成**: 可直接在 CLI 中查看已安装模型,或输入模型名(如 `llama3.1`)一键拉取(Pull)。支持模型列表交互切换和硬件状态自动检测。 @@ -172,6 +171,14 @@ bun run build - **配置校验**: 检查环境变量(如 `LOCAL_BASE_URL`)和权限设置。 - **故障排查**: 识别多个重复安装的版本、过期的版本锁或权限不足的更新。 +## 环境变量 (Environment Variables) + +除了交互式 `/login` 配置外,你也可以通过环境变量直接配置: + +- `LOCAL_BASE_URL`: 本地 LLM 运行地址 (例如 `http://localhost:11434`)。 +- `LOCAL_MODEL`: 本地 LLM 模型名称 (例如 `llama3.1`)。用于 `local` provider 时覆盖默认模型。 +- `GEMINI_BASE_URL`: Gemini API 的自定义基础地址。 + ## Feature Flags 所有功能开关通过 `FEATURE_=1` 环境变量启用,例如: diff --git a/README_EN.md b/README_EN.md index d2b24a910..186b6bafe 100644 --- a/README_EN.md +++ b/README_EN.md @@ -149,8 +149,7 @@ After running for the first time, type `/login` in the REPL to enter the login c 2. **OpenAI / Gemini / Grok**: Connect to cloud services using their respective protocols. - **Gemini (Google Auth)**: Supports interactive browser login. 1. In the Google Cloud Console, navigate to **APIs & Services > OAuth consent screen** and configure the OAuth client (Set User Type to External). - 2. Download the credentials JSON format and save it as `/.files/OAuth.json` in the project root. - 3. Leave the API Key field blank and press Enter in the `/login` configuration interface; the CLI will automatically open your browser for Google OAuth 2.0 authorization and fetch available models. + 2. Download the credentials JSON format and save it as `.files/OAuth.json` in the project root. 3. Leave the API Key field blank and press Enter in the `/login` configuration interface; the CLI will automatically open your browser for Google OAuth 2.0 authorization and fetch available models. 3. **Local LLM**: **(Recommended)** Use models running locally. - Supports **Ollama**, **LM Studio**, **Jan.ai**, **LocalAI**. - **Ollama Deep Integration**: View installed models directly in the CLI, or enter a model name (e.g., `llama3.1`) to pull it instantly. Supports interactive model selection navigation and hardware status auto-detection. @@ -158,6 +157,14 @@ After running for the first time, type `/login` in the REPL to enter the login c > ℹ️ Supports all Anthropic API compatible services (e.g., OpenRouter, AWS Bedrock proxies, etc.), as long as the interface is compatible with the Messages API. +## Environment Variables + +In addition to interactive `/login` configuration, you can also configure the CLI via environment variables: + +- `LOCAL_BASE_URL`: The base URL for the local LLM runner (e.g., `http://localhost:11434`). +- `LOCAL_MODEL`: The model name for the local LLM (e.g., `llama3.1`). Overrides the default model when using the `local` provider. +- `GEMINI_BASE_URL`: Custom base URL for the Gemini API. + ## Feature Flags All feature toggles are enabled via `FEATURE_=1` environment variables, for example: diff --git a/src/commands/provider.ts b/src/commands/provider.ts index e8c1f0bd4..8d9c6f09b 100644 --- a/src/commands/provider.ts +++ b/src/commands/provider.ts @@ -180,9 +180,10 @@ const provider = { type: 'local', name: 'provider', description: - 'Switch API provider (anthropic/openai/gemini/grok/bedrock/vertex/foundry)', + 'Switch API provider (anthropic|openai|gemini|grok|bedrock|vertex|foundry|local)', aliases: ['api'], - argumentHint: '[anthropic|openai|gemini|grok|bedrock|vertex|foundry|unset]', + argumentHint: + '[anthropic|openai|gemini|grok|bedrock|vertex|foundry|local|unset]', supportsNonInteractive: true, load: () => Promise.resolve({ call }), } satisfies Command diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 46f7cbdb4..98ca20d60 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -88,6 +88,7 @@ type OAuthStatus = } | { state: 'local_llm_pulling'; + baseUrl: string; modelName: string; status: string; percentage?: number; @@ -103,6 +104,8 @@ type OAuthStatus = toRetry?: OAuthStatus; }; +type LocalLlmSetupState = Extract; + const PASTE_HERE_MSG = 'Paste code here if prompted > '; const POPULAR_MODELS = ['llama3.1', 'mistral', 'phi3', 'qwen2', 'gemma2', 'codellama']; export function ConsoleOAuthFlow({ @@ -206,13 +209,10 @@ export function ConsoleOAuthFlow({ useEffect(() => { if (oauthStatus.state === 'local_llm_pulling') { const abortController = new AbortController(); + const { baseUrl, modelName } = oauthStatus; (async () => { try { - for await (const progress of pullOllamaModel( - oauthStatus.modelName, - 'http://localhost:11434', - abortController.signal, - )) { + for await (const progress of pullOllamaModel(modelName, baseUrl, abortController.signal)) { setOAuthStatus(prev => prev.state === 'local_llm_pulling' ? { @@ -227,8 +227,8 @@ export function ConsoleOAuthFlow({ setOAuthStatus({ state: 'local_llm_setup', runnerType: 'ollama', - baseUrl: 'http://localhost:11434', - modelName: oauthStatus.modelName, + baseUrl, + modelName, activeField: 'model_name', availableModels: [], isLoadingModels: false, @@ -718,7 +718,7 @@ function OAuthStatusMessage({ const displayValues: Record = { runner_type: oauthStatus.runnerType, base_url: oauthStatus.baseUrl, - api_key: (oauthStatus as any).apiKey ?? '', + api_key: oauthStatus.apiKey ?? '', model_name: oauthStatus.modelName, custom_model_name: oauthStatus.modelName, }; @@ -727,10 +727,10 @@ function OAuthStatusMessage({ const [localInputCursorOffset, setLocalInputCursorOffset] = useState((displayValues[activeField] ?? '').length); const buildLocalState = useCallback( - (field: LocalField, val: string, nextField?: LocalField): OAuthStatus => { - const newState = { ...oauthStatus } as any; + (field: LocalField, val: string, nextField?: LocalField): LocalLlmSetupState => { + const newState = { ...oauthStatus }; if (field === 'runner_type') { - newState.runnerType = val; + newState.runnerType = val as LocalLlmSetupState['runnerType']; if (val === 'ollama') newState.baseUrl = 'http://localhost:11434'; else if (val === 'lmstudio') newState.baseUrl = 'http://localhost:1234/v1'; else if (val === 'jan') newState.baseUrl = 'http://localhost:1337/v1'; @@ -749,7 +749,7 @@ function OAuthStatusMessage({ ); const doLocalSave = useCallback( - async (stateToSave: any) => { + async (stateToSave: LocalLlmSetupState) => { const { runnerType, baseUrl, modelName, apiKey } = stateToSave; const env: Record = { LOCAL_BASE_URL: baseUrl, @@ -761,11 +761,11 @@ function OAuthStatusMessage({ updateSettingsForSource('userSettings', { modelType: 'local', env, - } as any); + }); updateSettingsForSource('userSettings', { model: modelName || 'llama3.1', - } as any); + }); setOAuthStatus({ state: 'success' }); void onDone(); @@ -778,6 +778,7 @@ function OAuthStatusMessage({ if (oauthStatus.runnerType === 'ollama' && !oauthStatus.availableModels.includes(localInputValue)) { setOAuthStatus({ state: 'local_llm_pulling', + baseUrl: oauthStatus.baseUrl, modelName: localInputValue, status: 'Starting download...', }); @@ -797,7 +798,7 @@ function OAuthStatusMessage({ } else { // find next interactive field (skip model_name if custom_model_name is next, but that's handled by onChange) const next = LOCAL_FIELDS[idx + 1]!; - const nextState = buildLocalState(activeField, localInputValue, next) as any; + const nextState = buildLocalState(activeField, localInputValue, next); setOAuthStatus(nextState); const nextVal = nextState[ @@ -821,7 +822,7 @@ function OAuthStatusMessage({ const idx = LOCAL_FIELDS.indexOf(activeField); if (idx < LOCAL_FIELDS.length - 1) { const next = LOCAL_FIELDS[idx + 1]!; - const nextState = buildLocalState(activeField, localInputValue, next) as any; + const nextState = buildLocalState(activeField, localInputValue, next); setOAuthStatus(nextState); const nextVal = nextState[ @@ -847,7 +848,7 @@ function OAuthStatusMessage({ const idx = LOCAL_FIELDS.indexOf(activeField); if (idx > 0) { const next = LOCAL_FIELDS[idx - 1]!; - const nextState = buildLocalState(activeField, localInputValue, next) as any; + const nextState = buildLocalState(activeField, localInputValue, next); setOAuthStatus(nextState); const nextVal = nextState[ @@ -929,7 +930,7 @@ function OAuthStatusMessage({