From 65be3e3cf5173900a130dcc29ff7cafa6a039999 Mon Sep 17 00:00:00 2001 From: dgnsrekt Date: Fri, 14 Aug 2026 17:53:54 -0500 Subject: [PATCH] fix: tolerate unknown LM Studio reasoning levels `capabilities.reasoning` pinned `allowed_options` and `default` to `off|on|low|medium|high`. LM Studio now publishes an `xhigh` level, and because the enum sits inside the response schema a single model carrying it fails `LMStudioModelsResponseSchema.safeParse` for the entire payload. `discoverModels` then throws `unsupported response`, so discovery yields no models at all and the provider falls back to models.dev entries the user has not downloaded. The reasoning block is parsed but never mapped: the v1 contract leaves OpenCode's `reasoning` flag unset until both projects publish an interoperable mapping. Validating these values strictly gains nothing and costs total discovery whenever LM Studio adds a level. Accept any string and keep the known levels as an exported constant for documentation, so a future level cannot break discovery the same way. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S7Wjrr4wy4Eiig1PqtHfpD --- src/types/index.ts | 11 +++++++++-- test/plugin.test.ts | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index c41d9bb..d1dfb7d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,13 +9,20 @@ export const LMStudioLoadedInstanceSchema = z.looseObject({ }), }) +/** + * Reasoning levels LM Studio is known to publish. Kept as documentation only: + * the schema accepts any string so a level added by a newer LM Studio build + * cannot fail validation for the whole response. + */ +export const KNOWN_REASONING_LEVELS = ["off", "on", "low", "medium", "high"] as const + /** Capabilities reported for a native v1 LLM record. */ export const LMStudioCapabilitiesSchema = z.looseObject({ vision: z.boolean(), trained_for_tool_use: z.boolean(), reasoning: z.looseObject({ - allowed_options: z.array(z.enum(["off", "on", "low", "medium", "high"])), - default: z.enum(["off", "on", "low", "medium", "high"]), + allowed_options: z.array(z.string()), + default: z.string(), }).optional(), }) diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 4aa1446..584fea4 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -98,6 +98,26 @@ describe("LM Studio native API v1", () => { ) }) + it("keeps discovering when a model reports a reasoning level this release predates", async () => { + const fetcher = vi.fn(async () => modelsResponse([ + model({ + capabilities: { + vision: false, + trained_for_tool_use: true, + reasoning: { allowed_options: ["off", "low", "medium", "xhigh", "on"], default: "xhigh" }, + }, + }), + model({ key: "publisher/second", display_name: "Second" }), + ])) + + const response = await discoverModels("http://127.0.0.1:1234", { + fetch: fetcher as typeof fetch, + }) + + expect(response.models).toHaveLength(2) + expect(response.models[0]?.capabilities?.reasoning?.default).toBe("xhigh") + }) + it("rejects HTTP-200 error bodies instead of treating status as endpoint support", async () => { const fetcher = vi.fn(async () => new Response(JSON.stringify({ error: "Unexpected endpoint" }), { status: 200 }))