From 2fb407c7439c663f100f8b31ce0c7c1ec82719a0 Mon Sep 17 00:00:00 2001 From: Neeraj Date: Mon, 6 Jul 2026 12:05:01 +0530 Subject: [PATCH 1/5] feat(config): add lmStudio provider to config and type definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lmStudio to ProviderName union type, MercuryConfig providers interface, default config with baseUrl http://127.0.0.1:1234/v1, and isProviderConfigured (no API key required — checks baseUrl + model like ollamaLocal). --- src/utils/config.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/utils/config.ts b/src/utils/config.ts index f7ce2b49..1a9d3818 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -57,6 +57,7 @@ export type ProviderName = | 'openaiCompat' | 'mimo' | 'mimoTokenPlan' + | 'lmStudio' | 'chatgptWeb' | 'githubCopilot'; @@ -77,6 +78,7 @@ export interface MercuryConfig { openaiCompat: ProviderConfig; mimo: ProviderConfig; mimoTokenPlan: ProviderConfig; + lmStudio: ProviderConfig; chatgptWeb: ProviderConfig; githubCopilot: ProviderConfig; }; @@ -267,6 +269,13 @@ export function getDefaultConfig(): MercuryConfig { model: getEnv('MIMO_TOKEN_PLAN_MODEL', 'mimo-v2.5-pro'), enabled: getEnvBool('MIMO_TOKEN_PLAN_ENABLED', false), }, + lmStudio: { + name: 'lmStudio', + apiKey: getEnv('LM_STUDIO_API_KEY', ''), + baseUrl: getEnv('LM_STUDIO_BASE_URL', 'http://127.0.0.1:1234/v1'), + model: getEnv('LM_STUDIO_MODEL', ''), + enabled: getEnvBool('LM_STUDIO_ENABLED', false), + }, chatgptWeb: { name: 'chatgptWeb', apiKey: '', // not used — auth is via OAuth @@ -455,6 +464,9 @@ export function isProviderConfigured(provider: ProviderConfig): boolean { if (provider.name === 'openaiCompat') { return provider.baseUrl.length > 0 && provider.model.length > 0; } + if (provider.name === 'lmStudio') { + return provider.baseUrl.length > 0 && provider.model.length > 0; + } if (provider.name === 'chatgptWeb') { // ChatGPT Web uses browser session auth, not API keys. // Considered "configured" if enabled with a model selected. From ac0a2aada2f985e179cffc3724e0e6f36afabca5 Mon Sep 17 00:00:00 2001 From: Neeraj Date: Mon, 6 Jul 2026 12:05:05 +0530 Subject: [PATCH 2/5] feat(providers): route lmStudio through OpenAICompatProvider LM Studio exposes an OpenAI-compatible API at localhost:1234/v1. Route it through the existing OpenAICompatProvider with useChatApi: true, same as ollamaCloud and ollamaLocal. --- src/providers/registry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 2de70f09..18495a6f 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -18,7 +18,7 @@ async function createProvider(pc: ProviderConfig): Promise { // this entirely. const { OpenAICompatProvider } = await import('./openai-compat.js'); return new OpenAICompatProvider(pc, { useChatApi: true }); - } else if (pc.name === 'ollamaCloud' || pc.name === 'openaiCompat') { + } else if (pc.name === 'ollamaCloud' || pc.name === 'openaiCompat' || pc.name === 'lmStudio') { const { OpenAICompatProvider } = await import('./openai-compat.js'); return new OpenAICompatProvider(pc, { useChatApi: true }); } else if (pc.name === 'mimo' || pc.name === 'mimoTokenPlan') { From 9bef76ddc69bcb183a1f25c7cd54301fa3b65d0a Mon Sep 17 00:00:00 2001 From: Neeraj Date: Mon, 6 Jul 2026 12:05:10 +0530 Subject: [PATCH 3/5] feat(providers): add lmStudio model catalog and fetch routing Adds LM_STUDIO_PREFERRED_MODELS empty list (LM Studio models vary by user install), fetchLmStudioModels function targeting /v1/models endpoint, routing in fetchProviderModelCatalog, and 2 tests for lmStudio model catalog. --- src/utils/provider-models.test.ts | 20 ++++++++++++++++++++ src/utils/provider-models.ts | 21 +++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/utils/provider-models.test.ts b/src/utils/provider-models.test.ts index 74a464b5..3ad20f9f 100644 --- a/src/utils/provider-models.test.ts +++ b/src/utils/provider-models.test.ts @@ -116,4 +116,24 @@ describe('buildModelCatalog', () => { expect(catalog.recommendedModel).toBe('model-a'); }); + + it('uses the first model as recommended for LM Studio when no preferred list exists', () => { + const catalog = buildModelCatalog( + 'lmStudio', + ['llama-3.2-3b-instruct', 'mistral-7b-instruct-v0.3'], + ); + + expect(catalog.recommendedModel).toBe('llama-3.2-3b-instruct'); + expect(catalog.models).toContain('mistral-7b-instruct-v0.3'); + }); + + it('falls back to the current model for LM Studio when no preferred list exists', () => { + const catalog = buildModelCatalog( + 'lmStudio', + ['qwen2.5-7b-instruct'], + 'qwen2.5-7b-instruct', + ); + + expect(catalog.recommendedModel).toBe('qwen2.5-7b-instruct'); + }); }); diff --git a/src/utils/provider-models.ts b/src/utils/provider-models.ts index 3a50d38f..3ac34bec 100644 --- a/src/utils/provider-models.ts +++ b/src/utils/provider-models.ts @@ -72,6 +72,7 @@ const MIMO_PREFERRED_MODELS = [ const MIMO_TOKEN_PLAN_PREFERRED_MODELS = MIMO_PREFERRED_MODELS; const OPENAI_COMPAT_PREFERRED_MODELS = [] as const; +const LM_STUDIO_PREFERRED_MODELS = [] as const; const CHATGPT_WEB_PREFERRED_MODELS = [ 'gpt-5.5', @@ -199,6 +200,7 @@ function chooseRecommendedModel( mimo: MIMO_PREFERRED_MODELS, mimoTokenPlan: MIMO_TOKEN_PLAN_PREFERRED_MODELS, chatgptWeb: CHATGPT_WEB_PREFERRED_MODELS, + lmStudio: LM_STUDIO_PREFERRED_MODELS, githubCopilot: GITHUB_COPILOT_PREFERRED_MODELS, }; @@ -238,6 +240,7 @@ export function buildModelCatalog( mimoTokenPlan: MIMO_TOKEN_PLAN_PREFERRED_MODELS, chatgptWeb: CHATGPT_WEB_PREFERRED_MODELS, githubCopilot: GITHUB_COPILOT_PREFERRED_MODELS, + lmStudio: LM_STUDIO_PREFERRED_MODELS, }; const withoutRecommended = filtered.filter((model) => model !== recommendedModel); @@ -343,6 +346,20 @@ async function fetchOllamaCloudModels(config: ProviderConfig): Promise { + const data = await fetchJson( + `${trimTrailingSlash(config.baseUrl)}/models`, + {}, + 'Mercury could not fetch models from LM Studio. Make sure LM Studio is running and the Developer API is enabled.', + ); + + const ids = (data.data ?? []) + .map((model) => model.id?.trim() ?? '') + .filter(Boolean); + + return buildModelCatalog('lmStudio', ids, config.model); +} + async function fetchOllamaLocalModels(config: ProviderConfig): Promise { const data = await fetchJson( `${trimTrailingSlash(config.baseUrl)}/tags`, @@ -423,6 +440,10 @@ export async function fetchProviderModelCatalog( return fetchOpenAICompatModels(provider, config); } + if (provider === 'lmStudio') { + return fetchLmStudioModels(config); + } + if (provider === 'mimo') { return fetchMiMoModels(config); } From 60589ad42448c9588e8ec92b651db47374471c18 Mon Sep 17 00:00:00 2001 From: Neeraj Date: Mon, 6 Jul 2026 12:05:15 +0530 Subject: [PATCH 4/5] feat(cli): add lmStudio to setup wizard and provider options Adds lmStudio to PROVIDER_OPTIONS list, validates no API key needed (returns null), adds promptLmStudioModelSelection function (cloned from ollamaLocal pattern), and wires it into the setup wizard flow. --- src/index.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/index.ts b/src/index.ts index fc7d63f3..35ab1843 100644 --- a/src/index.ts +++ b/src/index.ts @@ -152,6 +152,7 @@ const PROVIDER_OPTIONS: Array<{ key: ProviderName; label: string }> = [ { key: 'openaiCompat', label: 'OpenAI Compilations' }, { key: 'mimo', label: 'MiMo (Xiaomi)' }, { key: 'mimoTokenPlan', label: 'MiMo Token Plan (Xiaomi)' }, + { key: 'lmStudio', label: 'LM Studio' }, ]; function getConfiguredProviderNames(config: MercuryConfig): ProviderName[] { @@ -295,6 +296,10 @@ function validateApiKey(provider: ProviderName, value: string): string | null { : 'Ollama Cloud keys must look like a real API token: long, no spaces, and not plain text.'; } + if (provider === 'lmStudio') { + return null; + } + if (provider === 'mimo') { return /^sk-[A-Za-z0-9_-]{16,}$/i.test(value) ? null @@ -500,6 +505,64 @@ async function promptOllamaLocalModelSelection(config: MercuryConfig, isReconfig } } +async function promptLmStudioModelSelection(config: MercuryConfig, isReconfig: boolean): Promise<{ baseUrl?: string; model?: string; skipped: boolean }> { + const existingConfig = config.providers.lmStudio; + + const baseUrlPrompt = isReconfig && existingConfig.baseUrl + ? chalk.white(` LM Studio base URL [${existingConfig.baseUrl}]: `) + : chalk.white(' LM Studio base URL [Enter for http://127.0.0.1:1234/v1]: '); + const baseUrlInput = await ask(baseUrlPrompt); + if (!baseUrlInput || baseUrlInput.toLowerCase() === 'none') { + if (isReconfig && existingConfig.baseUrl) { + return { baseUrl: existingConfig.baseUrl, model: existingConfig.model, skipped: true }; + } + return { skipped: true }; + } + const baseUrlError = validateBaseUrl(baseUrlInput); + if (baseUrlError) { + console.log(chalk.red(` ${baseUrlError}`)); + if (isReconfig && existingConfig.baseUrl) { + return { baseUrl: existingConfig.baseUrl, model: existingConfig.model, skipped: true }; + } + return { skipped: true }; + } + const baseUrl = baseUrlInput; + + console.log(chalk.dim(' Fetching LM Studio models...')); + try { + const catalog = await fetchProviderModelCatalog('lmStudio', { + ...existingConfig, + baseUrl, + }); + const model = await chooseProviderModel( + 'LM Studio', + catalog.recommendedModel, + catalog.models, + ); + return { baseUrl, model, skipped: false }; + } catch (error) { + const message = error instanceof ProviderModelFetchError + ? error.message + : 'Mercury could not fetch LM Studio models.'; + console.log(chalk.yellow(` ${message}`)); + console.log(chalk.dim(' Make sure LM Studio is running and the Developer API is enabled.')); + console.log(chalk.dim(' You can run `mercury doctor` later to configure LM Studio after starting it.')); + + const manualModel = await ask(chalk.white(` LM Studio model name (Enter to skip LM Studio for now): `)); + if (!manualModel) { + return { skipped: true }; + } + + const modelError = validateModelName(manualModel); + if (modelError) { + console.log(chalk.red(` ${modelError}`)); + return { skipped: true }; + } + + return { baseUrl, model: manualModel, skipped: false }; + } +} + async function promptOpenAICompatSetup(config: MercuryConfig, isReconfig: boolean): Promise<{ baseUrl?: string; apiKey?: string; model?: string; skipped: boolean }> { const existingConfig = config.providers.openaiCompat; @@ -1281,6 +1344,16 @@ async function configure(existingConfig?: MercuryConfig): Promise { continue; } + + if (provider === 'lmStudio') { + const result = await promptLmStudioModelSelection(config, isReconfig); + if (!result.skipped && result.baseUrl && result.model) { + config.providers.lmStudio.baseUrl = result.baseUrl; + config.providers.lmStudio.model = result.model; + config.providers.lmStudio.enabled = true; + } + continue; + } if (provider === 'openaiCompat') { const result = await promptOpenAICompatSetup(config, isReconfig); if (!result.skipped && result.baseUrl && result.model) { From abee454969c0c208c601c6ea13d34227e79b4ee9 Mon Sep 17 00:00:00 2001 From: Neeraj Date: Mon, 6 Jul 2026 12:05:20 +0530 Subject: [PATCH 5/5] chore: add LM Studio environment variables to .env.example Adds LM_STUDIO_BASE_URL (default http://127.0.0.1:1234/v1), LM_STUDIO_MODEL, and LM_STUDIO_ENABLED env vars. --- .env.example | 5 +++++ package-lock.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index b89e497f..3d7ec568 100644 --- a/.env.example +++ b/.env.example @@ -53,6 +53,11 @@ MIMO_MODEL=mimo-v2.5-pro MIMO_TOKEN_PLAN_API_KEY= MIMO_TOKEN_PLAN_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1 MIMO_TOKEN_PLAN_MODEL=mimo-v2.5-pro + +# LM Studio (local desktop app, no API key required) +LM_STUDIO_BASE_URL=http://127.0.0.1:1234/v1 +LM_STUDIO_MODEL= +LM_STUDIO_ENABLED=false MIMO_TOKEN_PLAN_ENABLED=false # Default provider: deepseek | openai | anthropic | grok | ollamaCloud | ollamaLocal | openaiCompat | mimo | mimoTokenPlan diff --git a/package-lock.json b/package-lock.json index 2948a6ab..57058d6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cosmicstack/mercury-agent", - "version": "1.1.12", + "version": "1.1.13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cosmicstack/mercury-agent", - "version": "1.1.12", + "version": "1.1.13", "license": "MIT", "dependencies": { "@ai-sdk/anthropic": "^3.0.71",