diff --git a/plugins/fusion-plugin-omp-runtime/package.json b/plugins/fusion-plugin-omp-runtime/package.json index ce886e9cf4..73088b8eb3 100644 --- a/plugins/fusion-plugin-omp-runtime/package.json +++ b/plugins/fusion-plugin-omp-runtime/package.json @@ -25,7 +25,7 @@ }, "private": true, "scripts": { - "build": "tsc", + "build": "tsc && node -e \"require('node:fs').copyFileSync('src/mcp-schema-server.cjs','dist/mcp-schema-server.cjs')\"", "typecheck": "tsc --noEmit", "test": "vitest run --silent=passed-only --reporter=dot" }, diff --git a/plugins/fusion-plugin-omp-runtime/src/index.ts b/plugins/fusion-plugin-omp-runtime/src/index.ts index 79f1b5f869..52adf32e4d 100644 --- a/plugins/fusion-plugin-omp-runtime/src/index.ts +++ b/plugins/fusion-plugin-omp-runtime/src/index.ts @@ -91,6 +91,7 @@ const plugin: FusionPlugin = definePlugin({ export default plugin; export { probeOmpBinary } from "./probe.js"; export { discoverOmpProviderModels } from "./provider.js"; +export { discoverOmpModels, resolveOmpModelSelector } from "./process-manager.js"; export { OmpRuntimeAdapter } from "./runtime-adapter.js"; export type { OmpBinaryStatus } from "./types.js"; export { diff --git a/plugins/fusion-plugin-omp-runtime/src/process-manager.ts b/plugins/fusion-plugin-omp-runtime/src/process-manager.ts index d4097c8715..a89e2e862a 100644 --- a/plugins/fusion-plugin-omp-runtime/src/process-manager.ts +++ b/plugins/fusion-plugin-omp-runtime/src/process-manager.ts @@ -5,6 +5,13 @@ FNXC:OmpAcp 2026-07-11-23:35: Model discovery for the omp-cli provider card. Prefer a structured list when available; fall soft to an empty list with a clear reason so the picker stays usable without inventing model ids. + +FNXC:OmpAcp 2026-07-18-16:40: +Bare table ids like MiniMax-M2.5 are ambiguous across omp providers +(minimax-code vs alibaba-coding-plan). Prefer `omp models --json` `selector` +fields (`minimax-code/MiniMax-M2.5`) so ACP `--model` hits the authenticated +provider instead of failing with "No API key found for alibaba-coding-plan" / +JSON-RPC Internal error. */ export interface OmpModelDiscoveryResult { @@ -16,12 +23,20 @@ export interface OmpModelDiscoveryResult { /** * Attempt to list models from the local omp install. - * Tries `omp models` then falls back to empty (CLI default still works via ACP). + * Prefers `omp models --json` selectors, then text table, then empty. */ export async function discoverOmpModels( binary: string, timeoutMs = 8000, ): Promise { + const jsonResult = await runOmpCommand(binary, ["models", "--json"], timeoutMs); + if (jsonResult.code === 0) { + const models = parseModelListJson(jsonResult.stdout || jsonResult.stderr); + if (models.length > 0) { + return { models, source: "omp models --json", fallbackUsed: false }; + } + } + const result = await runOmpCommand(binary, ["models"], timeoutMs); if (result.code === 0) { const models = parseModelList(result.stdout || result.stderr); @@ -42,6 +57,70 @@ export async function discoverOmpModels( }; } +/** + * FNXC:OmpAcp 2026-07-18-16:40: + * Map a bare or omp-cli-prefixed model id to a unique provider-qualified selector + * from discovery (e.g. MiniMax-M2.5 → minimax-code/MiniMax-M2.5). Returns the + * original id when already qualified, unique, or discovery is empty. + */ +export async function resolveOmpModelSelector( + binary: string, + model: string | undefined, + timeoutMs = 8000, +): Promise { + const normalized = model?.trim(); + if (!normalized) return undefined; + if (normalized.includes("/")) return normalized; + + const discovered = await discoverOmpModels(binary, timeoutMs); + if (discovered.models.length === 0) return normalized; + + const exact = discovered.models.filter((id) => id === normalized); + if (exact.length === 1) return exact[0]; + + const qualified = discovered.models.filter((id) => id.endsWith(`/${normalized}`)); + if (qualified.length === 1) return qualified[0]; + if (qualified.length > 1) { + // Prefer common coding providers when bare ids collide across plans. + const preferred = + qualified.find((id) => id.startsWith("minimax-code/")) + ?? qualified.find((id) => !id.includes("alibaba") && !id.includes("coding-plan")) + ?? qualified[0]; + return preferred; + } + + return normalized; +} + +function parseModelListJson(text: string): string[] { + try { + const data = JSON.parse(text) as unknown; + const list = Array.isArray(data) + ? data + : data && typeof data === "object" && Array.isArray((data as { models?: unknown }).models) + ? (data as { models: unknown[] }).models + : null; + if (!list) return []; + + const models: string[] = []; + const seen = new Set(); + for (const entry of list) { + if (!entry || typeof entry !== "object") continue; + const record = entry as Record; + const selector = typeof record.selector === "string" ? record.selector.trim() : ""; + const provider = typeof record.provider === "string" ? record.provider.trim() : ""; + const id = typeof record.id === "string" ? record.id.trim() : ""; + const candidate = selector || (provider && id ? `${provider}/${id}` : id); + if (!candidate || candidate.length < 2 || seen.has(candidate)) continue; + seen.add(candidate); + models.push(candidate); + } + return models; + } catch { + return []; + } +} + function parseModelList(text: string): string[] { const lines = text.split(/\r?\n/); const models: string[] = []; diff --git a/plugins/fusion-plugin-omp-runtime/src/runtime-adapter.ts b/plugins/fusion-plugin-omp-runtime/src/runtime-adapter.ts index e6e51355c2..11fa363a75 100644 --- a/plugins/fusion-plugin-omp-runtime/src/runtime-adapter.ts +++ b/plugins/fusion-plugin-omp-runtime/src/runtime-adapter.ts @@ -5,6 +5,7 @@ import { normalizeOmpCliModel, } from "./acp-settings.js"; import { toAcpMcpServers, type AcpMcpServer } from "./mcp-forwarding.js"; +import { resolveOmpModelSelector } from "./process-manager.js"; import { startFusionToolBridge, type FusionToolBridge, @@ -32,6 +33,11 @@ Grok ACP: - Operator MCP servers → session/new.mcpServers (stdio/http/sse) - Engine customTools (fn_*) → loopback MCP bridge + fusion-custom-tools server - System rules describe available Fusion MCP tools so omp prefers them for board ops + +FNXC:OmpAcp 2026-07-18-16:40: +Resolve bare picker ids (MiniMax-M2.5) to provider-qualified selectors before +`omp --model … acp` so multi-provider collisions do not land on an unauthenticated +plan provider and return JSON-RPC Internal error. */ export type AcpAdapterFactory = (settings: Record) => { @@ -246,7 +252,16 @@ export class OmpRuntimeAdapter implements AgentRuntime { systemPrompt: "", }, ): Promise { - const model = normalizeOmpCliModel(options.defaultModelId) ?? "omp/default"; + const rawModel = normalizeOmpCliModel(options.defaultModelId) ?? "omp/default"; + /* + FNXC:OmpAcp 2026-07-18-16:40: + Bare ids from the omp-cli picker can map to multiple upstream providers. + Resolve to a unique `provider/id` selector before spawn so authenticated + providers (e.g. minimax-code) win over unauthenticated plan aliases. + */ + const model = + (await resolveOmpModelSelector(this.binary, modelForCli(rawModel) ?? rawModel)) + ?? rawModel; const turnAccum: TurnAccum = { text: "" }; const resources: SessionResources = {};