From 0144926721d3ec5c2d9141159fe8278441b7aacd Mon Sep 17 00:00:00 2001 From: Trent Pierce Date: Mon, 2 Mar 2026 08:51:42 -0600 Subject: [PATCH 1/3] feat(lmstudio): add dynamic model discovery and configurable server address --- packages/opencode/src/auth/index.ts | 1 + packages/opencode/src/cli/cmd/auth.ts | 37 +++++++++-- packages/opencode/src/provider/provider.ts | 71 +++++++++++++++++++--- 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 776cc99b444..4f61fcc7226 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -21,6 +21,7 @@ export namespace Auth { .object({ type: z.literal("api"), key: z.string(), + baseURL: z.string().optional(), }) .meta({ ref: "ApiAuth" }) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 95635916413..8c2d280fea2 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -198,7 +198,7 @@ export const AuthCommand = cmd({ describe: "manage credentials", builder: (yargs) => yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(), - async handler() {}, + async handler() { }, }) export const AuthListCommand = cmd({ @@ -288,7 +288,7 @@ export const AuthLoginCommand = cmd({ prompts.outro("Done") return } - await ModelsDev.refresh().catch(() => {}) + await ModelsDev.refresh().catch(() => { }) const config = await Config.get() @@ -386,10 +386,10 @@ export const AuthLoginCommand = cmd({ if (provider === "amazon-bedrock") { prompts.log.info( "Amazon Bedrock authentication priority:\n" + - " 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" + - " 2. AWS credential chain (profile, access keys, IAM roles, EKS IRSA)\n\n" + - "Configure via opencode.json options (profile, region, endpoint) or\n" + - "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_WEB_IDENTITY_TOKEN_FILE).", + " 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" + + " 2. AWS credential chain (profile, access keys, IAM roles, EKS IRSA)\n\n" + + "Configure via opencode.json options (profile, region, endpoint) or\n" + + "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_WEB_IDENTITY_TOKEN_FILE).", ) } @@ -407,6 +407,31 @@ export const AuthLoginCommand = cmd({ ) } + if (provider === "lmstudio") { + const baseURL = await prompts.text({ + message: "Enter LM Studio server address", + placeholder: "http://127.0.0.1:1234/v1", + defaultValue: "http://127.0.0.1:1234/v1", + validate: (x) => (x && x.startsWith("http") ? undefined : "Must start with http:// or https://"), + }) + if (prompts.isCancel(baseURL)) throw new UI.CancelledError() + + const key = await prompts.password({ + message: "Enter LM Studio API key (optional)", + placeholder: "Leave empty if not required", + }) + if (prompts.isCancel(key)) throw new UI.CancelledError() + + await Auth.set(provider, { + type: "api", + key: key || "sk-nothing", + baseURL, + }) + + prompts.outro("Done") + return + } + const key = await prompts.password({ message: "Enter your API key", validate: (x) => (x && x.length > 0 ? undefined : "Required"), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 022ec316795..1ee7b3705bc 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -547,7 +547,7 @@ export namespace Provider { if (!apiToken) { throw new Error( "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " + - "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.", + "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.", ) } @@ -588,6 +588,61 @@ export namespace Provider { }, } }, + lmstudio: async (provider) => { + const auth = await Auth.get("lmstudio") + const baseURL = + (auth?.type === "api" ? auth.baseURL : undefined) ?? "http://127.0.0.1:1234/v1" + const apiKey = auth?.type === "api" ? auth.key : undefined + + try { + const response = await fetch(`${baseURL}/models`, { + headers: apiKey && apiKey !== "sk-nothing" ? { Authorization: `Bearer ${apiKey}` } : {}, + signal: AbortSignal.timeout(2000), + }) + if (response.ok) { + const json = (await response.json()) as { data: { id: string }[] } + if (Array.isArray(json.data)) { + for (const m of json.data) { + if (!provider.models[m.id]) { + provider.models[m.id] = { + id: m.id, + name: `${m.id} (local)`, + providerID: "lmstudio", + api: { + id: m.id, + url: baseURL, + npm: "@ai-sdk/openai-compatible", + }, + capabilities: { + temperature: true, + reasoning: false, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 32000, output: 4096 }, + headers: {}, + options: {}, + release_date: "", + status: "active", + family: "", + } + } + } + } + } + } catch (e) { + log.error("Failed to discover LM Studio models", { error: e }) + } + + return { + autoload: !!auth, + options: { baseURL, apiKey }, + } + }, } export const Model = z @@ -699,13 +754,13 @@ export namespace Provider { }, experimentalOver200K: model.cost?.context_over_200k ? { - cache: { - read: model.cost.context_over_200k.cache_read ?? 0, - write: model.cost.context_over_200k.cache_write ?? 0, - }, - input: model.cost.context_over_200k.input, - output: model.cost.context_over_200k.output, - } + cache: { + read: model.cost.context_over_200k.cache_read ?? 0, + write: model.cost.context_over_200k.cache_write ?? 0, + }, + input: model.cost.context_over_200k.input, + output: model.cost.context_over_200k.output, + } : undefined, }, limit: { From dc4c53925e6cf54f3246dad95a7d25a2e68625a8 Mon Sep 17 00:00:00 2001 From: Trent Pierce Date: Mon, 2 Mar 2026 09:18:55 -0600 Subject: [PATCH 2/3] feat(lmstudio): dynamically fetch context limits from LM Studio --- packages/opencode/src/provider/provider.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 1ee7b3705bc..0e333b4f069 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -600,10 +600,16 @@ export namespace Provider { signal: AbortSignal.timeout(2000), }) if (response.ok) { - const json = (await response.json()) as { data: { id: string }[] } + const json = (await response.json()) as { + data: { id: string; max_context_length?: number }[] + } if (Array.isArray(json.data)) { for (const m of json.data) { if (!provider.models[m.id]) { + const discoveredContext = m.max_context_length ?? 128000 + const context = Math.max(discoveredContext, 8192) // Floor at 8k for stability + const output = Math.min(Math.floor(context / 4), 16384) + provider.models[m.id] = { id: m.id, name: `${m.id} (local)`, @@ -623,13 +629,19 @@ export namespace Provider { interleaved: false, }, cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, - limit: { context: 32000, output: 4096 }, + limit: { context, output }, headers: {}, options: {}, release_date: "", status: "active", family: "", } + + if (context < 32768) { + log.warn("LM Studio model has a small context limit, which may cause errors with OpenCode's large system prompt. Consider increasing it in LM Studio settings.", { id: m.id, context }) + } else { + log.info("Discovered LM Studio model", { id: m.id, context }) + } } } } From c87eedf041c2a718e7c3cde2fc435462b489a7ea Mon Sep 17 00:00:00 2001 From: Trent Pierce Date: Mon, 2 Mar 2026 09:36:29 -0600 Subject: [PATCH 3/3] fix(opencode): remove unsupported placeholder from password prompt --- packages/opencode/src/cli/cmd/auth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 8c2d280fea2..ca8dd264757 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -418,7 +418,6 @@ export const AuthLoginCommand = cmd({ const key = await prompts.password({ message: "Enter LM Studio API key (optional)", - placeholder: "Leave empty if not required", }) if (prompts.isCancel(key)) throw new UI.CancelledError()