diff --git a/docs/models.md b/docs/models.md index a1ca6d89f..00e7b7dea 100644 --- a/docs/models.md +++ b/docs/models.md @@ -272,6 +272,8 @@ If `lm-studio` is not explicitly configured, registry adds an implicit discovera Runtime discovery fetches models (`GET /models`) and synthesizes model entries with local defaults. +This path also works for local OpenAI-compatible servers that are not LM Studio. For example, set `LM_STUDIO_BASE_URL=http://127.0.0.1:11434/v1` to discover oMLX through the existing `/v1/models` flow. Do not configure oMLX as `ollama`: Ollama discovery uses native `/api/tags` and `/api/show` endpoints, not OpenAI `/v1/models`. + ### Explicit provider discovery You can configure discovery yourself: @@ -606,6 +608,18 @@ providers: name: Qwen 2.5 Coder 32B (local) ``` +For oMLX or another local OpenAI-compatible server with a discoverable `/v1/models` endpoint, prefer discovery instead of listing models by hand: + +```yaml +providers: + omlx: + baseUrl: http://127.0.0.1:11434/v1 + auth: none + api: openai-completions + discovery: + type: openai-models-list +``` + ### Hosted proxy with env-based key ```yaml diff --git a/packages/coding-agent/test/lm-studio-fix.test.ts b/packages/coding-agent/test/lm-studio-fix.test.ts index 53992800d..63a5d6acd 100644 --- a/packages/coding-agent/test/lm-studio-fix.test.ts +++ b/packages/coding-agent/test/lm-studio-fix.test.ts @@ -16,13 +16,17 @@ describe("ModelRegistry LM Studio Fixes", () => { tempDir = path.join(os.tmpdir(), `pi-test-lm-studio-fixes-${Snowflake.next()}`); fs.mkdirSync(tempDir, { recursive: true }); modelsJsonPath = path.join(tempDir, "models.json"); - authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); + authStorage = await AuthStorage.create(":memory:"); }); afterEach(() => { authStorage.close(); if (tempDir && fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true }); + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EBUSY") throw error; + } } }); @@ -59,4 +63,37 @@ describe("ModelRegistry LM Studio Fixes", () => { expect(available.some(m => m.provider === "ollama")).toBe(true); expect(available.some(m => m.provider === "lm-studio")).toBe(true); }); + + test("LM_STUDIO_BASE_URL can target any local OpenAI-compatible /v1 server", async () => { + const originalBaseUrl = Bun.env.LM_STUDIO_BASE_URL; + Bun.env.LM_STUDIO_BASE_URL = "http://127.0.0.1:11434/v1"; + let requestedUrl = ""; + try { + const fetchMock: FetchImpl = input => { + const url = String(input); + if (url.includes(":11434/v1/models")) { + requestedUrl = url; + return Promise.resolve( + new Response(JSON.stringify({ data: [{ id: "omlx-model" }] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + } + return Promise.resolve(new Response(null, { status: 404 })); + }; + + const registry = new ModelRegistry(authStorage, modelsJsonPath, { fetch: fetchMock }); + await registry.refresh(); + + expect(requestedUrl).toBe("http://127.0.0.1:11434/v1/models"); + expect(registry.getAll().some(m => m.provider === "lm-studio" && m.id === "omlx-model")).toBe(true); + } finally { + if (originalBaseUrl === undefined) { + delete Bun.env.LM_STUDIO_BASE_URL; + } else { + Bun.env.LM_STUDIO_BASE_URL = originalBaseUrl; + } + } + }); });