From 5693e014ecea3f79700505d31de05b5199d1cee5 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 23 Jul 2026 19:19:47 +0200 Subject: [PATCH] extension: add oMLX model discovery --- extensions/.gitkeep | 0 extensions/omlx-models.ts | 118 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) delete mode 100644 extensions/.gitkeep create mode 100644 extensions/omlx-models.ts diff --git a/extensions/.gitkeep b/extensions/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/extensions/omlx-models.ts b/extensions/omlx-models.ts new file mode 100644 index 0000000..a53c743 --- /dev/null +++ b/extensions/omlx-models.ts @@ -0,0 +1,118 @@ +import { readFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +type OmlxSettings = { + server?: { host?: string; port?: number }; + auth?: { api_key?: string }; + sampling?: { max_context_window?: number; max_tokens?: number }; +}; + +type ListedModel = { + id: string; + max_model_len?: number | null; +}; + +type ModelStatus = { + id: string; + model_type?: string | null; + thinking_default?: boolean | null; + preserve_thinking_default?: boolean | null; + max_context_window?: number | null; + max_tokens?: number | null; +}; + +const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; + +function maySupportReasoning(id: string): boolean { + return /(?:thinking|reasoning|deepseek[-_. ]?r1|\b[or][13]\b|qwen3(?:[._-][56])?)/i.test(id); +} + +export default async function (pi: ExtensionAPI) { + const settingsPath = join(homedir(), ".omlx", "settings.json"); + + try { + const settings = JSON.parse(await readFile(settingsPath, "utf8")) as OmlxSettings; + const configuredHost = settings.server?.host ?? "127.0.0.1"; + const host = configuredHost === "0.0.0.0" || configuredHost === "::" ? "127.0.0.1" : configuredHost; + const port = settings.server?.port ?? 8127; + const apiKey = settings.auth?.api_key || "omlx"; + const baseUrl = `http://${host}:${port}/v1`; + const headers = { Authorization: `Bearer ${apiKey}` }; + const fetchOptions = { headers, signal: AbortSignal.timeout(5000) }; + + const modelsResponse = await fetch(`${baseUrl}/models`, fetchOptions); + if (!modelsResponse.ok) { + throw new Error(`${modelsResponse.status} ${modelsResponse.statusText}`); + } + + const listed = (await modelsResponse.json()) as { data?: ListedModel[] }; + + let statuses = new Map(); + try { + const statusResponse = await fetch(`${baseUrl}/models/status`, fetchOptions); + if (statusResponse.ok) { + const payload = (await statusResponse.json()) as { models?: ModelStatus[] }; + statuses = new Map((payload.models ?? []).map((model) => [model.id, model])); + } + } catch { + // /v1/models is sufficient; status only enriches capabilities and limits. + } + + const models = (listed.data ?? []) + .filter((model) => { + if (model.id === "MarkItDown") return false; + const type = statuses.get(model.id)?.model_type; + return type == null || type === "llm" || type === "vlm"; + }) + .map((model) => { + const status = statuses.get(model.id); + const reasoning = status?.thinking_default != null || maySupportReasoning(model.id); + const compat: Record = { + supportsDeveloperRole: false, + supportsReasoningEffort: false, + }; + + if (reasoning) { + compat.thinkingFormat = "qwen-chat-template"; + compat.chatTemplateKwargs = { + enable_thinking: { $var: "thinking.enabled" }, + preserve_thinking: status?.preserve_thinking_default === true, + }; + } + + return { + id: model.id, + name: `${model.id} (oMLX Local)`, + reasoning, + input: status?.model_type === "vlm" ? (["text", "image"] as const) : (["text"] as const), + cost: ZERO_COST, + contextWindow: + status?.max_context_window ?? + model.max_model_len ?? + settings.sampling?.max_context_window ?? + 128000, + maxTokens: status?.max_tokens ?? settings.sampling?.max_tokens ?? 16384, + compat, + }; + }); + + if (models.length === 0) { + console.warn("oMLX model discovery returned no chat models"); + return; + } + + pi.registerProvider("omlx", { + name: "oMLX (Local)", + baseUrl, + api: "openai-completions", + apiKey, + authHeader: true, + models, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(`Could not discover oMLX models from ~/.omlx: ${message}`); + } +}