FN-8262: route OMP models through ACP runtime
Route omp-cli model selections to the bundled OMP ACP runtime instead of pi registry resolution. - Derive and validate OMP runtime routing for primary and fallback model selections. - Preserve mock and test-mode short-circuits and provide actionable plugin remediation. - Add OMP routing regression coverage and a patch changeset. Files changed: .changeset/fn-8262-omp-model-routing.md | 7 + .../agent-session-helpers-omp-routing.test.ts | 218 +++++++++++++++++++++ packages/engine/src/agent-session-helpers.ts | 87 +++++++- 3 files changed, 306 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8262 Fusion-Task-Lineage: 7a77031f-8703-484e-9e53-a2ef097ec17e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8262-omp-model-routing.md
Normal file
7
.changeset/fn-8262-omp-model-routing.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Oh My Pi (omp) model selections now run via the OMP ACP runtime instead of failing.
|
||||
category: fix
|
||||
dev: agent-session-helpers auto-derives runtime hint "omp" for omp-cli primary/fallback selections (mirrors the Grok CLI no-visible-key seam); short-circuits under test mode/mock provider, validates an explicit "omp" hint against runtime availability, and prevents the "not found in the pi model registry" hard-fail. Throws an actionable error when the OMP runtime plugin is unavailable.
|
||||
@@ -0,0 +1,218 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as fusionCore from "@fusion/core";
|
||||
import { createResolvedAgentSession } from "../agent-session-helpers.js";
|
||||
import { MOCK_PROVIDER_ID } from "../providers/mock-provider.js";
|
||||
|
||||
const mockCreateFnAgent = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../pi.js", () => ({
|
||||
createFnAgent: mockCreateFnAgent,
|
||||
promptWithFallback: vi.fn().mockResolvedValue(undefined),
|
||||
describeModel: vi.fn().mockReturnValue("pi/default"),
|
||||
wrapToolsWithActionGate: vi.fn((tools) => tools),
|
||||
wrapToolsWithPermanentAgentGating: vi.fn((tools) => tools),
|
||||
wrapToolsWithRtkRewrite: vi.fn((tools) => tools),
|
||||
}));
|
||||
|
||||
/*
|
||||
FNXC:OmpAcp 2026-07-18-09:00:
|
||||
FN-8262 regression coverage keeps `omp-cli` selections out of pi's model registry: primary/fallback selections use the ACP runtime, unavailable runtime states provide the plugin remediation, and mock/test mode makes no OMP lookup.
|
||||
*/
|
||||
function makeOmpPluginRunnerStub(options?: { includeOmp?: boolean; includeOther?: boolean }) {
|
||||
const createSession = vi.fn().mockResolvedValue({
|
||||
session: { model: "omp/MiniMax-M2.5", messages: [], dispose: vi.fn() },
|
||||
});
|
||||
const ompRegistration = {
|
||||
pluginId: "fusion-plugin-omp-runtime",
|
||||
runtime: {
|
||||
metadata: { runtimeId: "omp", name: "OMP Runtime" },
|
||||
factory: vi.fn().mockResolvedValue({
|
||||
id: "omp",
|
||||
name: "OMP Runtime",
|
||||
createSession,
|
||||
promptWithFallback: vi.fn(),
|
||||
describeModel: vi.fn(() => "omp/MiniMax-M2.5"),
|
||||
}),
|
||||
},
|
||||
};
|
||||
const otherRegistration = {
|
||||
pluginId: "other-runtime",
|
||||
runtime: {
|
||||
metadata: { runtimeId: "other", name: "Other Runtime" },
|
||||
factory: vi.fn().mockResolvedValue({
|
||||
id: "other",
|
||||
name: "Other Runtime",
|
||||
createSession,
|
||||
promptWithFallback: vi.fn(),
|
||||
describeModel: vi.fn(() => "other/model"),
|
||||
}),
|
||||
},
|
||||
};
|
||||
const getRuntimeById = vi.fn((runtimeId: string) => {
|
||||
if (runtimeId === "omp" && options?.includeOmp !== false) return ompRegistration;
|
||||
if (runtimeId === "other" && options?.includeOther) return otherRegistration;
|
||||
return undefined;
|
||||
});
|
||||
return {
|
||||
pluginRunner: {
|
||||
getRuntimeById,
|
||||
createRuntimeContext: vi.fn().mockResolvedValue({
|
||||
pluginId: "fusion-plugin-omp-runtime",
|
||||
taskStore: {},
|
||||
settings: {},
|
||||
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
||||
emitEvent: vi.fn(),
|
||||
}),
|
||||
},
|
||||
getRuntimeById,
|
||||
createSession,
|
||||
};
|
||||
}
|
||||
|
||||
function sessionOptions(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
sessionPurpose: "executor" as const,
|
||||
cwd: "/tmp/project",
|
||||
systemPrompt: "system",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("createResolvedAgentSession OMP runtime routing", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
mockCreateFnAgent.mockReset().mockResolvedValue({
|
||||
session: { model: "pi/default", messages: [], dispose: vi.fn() },
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["MiniMax-M2.5", "omp-cli/MiniMax-M2.5"])(
|
||||
"routes primary omp-cli model %s through OMP ACP instead of pi registry",
|
||||
async (defaultModelId) => {
|
||||
const { pluginRunner, getRuntimeById, createSession } = makeOmpPluginRunnerStub();
|
||||
const audit = { database: vi.fn().mockResolvedValue(undefined) };
|
||||
|
||||
const result = await createResolvedAgentSession(sessionOptions({
|
||||
pluginRunner: pluginRunner as never,
|
||||
runAuditor: audit as never,
|
||||
defaultProvider: "omp-cli",
|
||||
defaultModelId,
|
||||
}));
|
||||
|
||||
expect(result.runtimeId).toBe("omp");
|
||||
expect(getRuntimeById).toHaveBeenCalledWith("omp");
|
||||
expect(createSession).toHaveBeenCalledWith(expect.objectContaining({
|
||||
defaultProvider: "omp-cli",
|
||||
defaultModelId: "MiniMax-M2.5",
|
||||
}));
|
||||
expect(mockCreateFnAgent).not.toHaveBeenCalled();
|
||||
expect(audit.database).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "session:runtime-resolved",
|
||||
target: "omp",
|
||||
metadata: expect.objectContaining({ reason: "omp-cli-runtime" }),
|
||||
}));
|
||||
},
|
||||
);
|
||||
|
||||
it("promotes an omp-cli fallback pair and thinking level into the OMP session", async () => {
|
||||
const { pluginRunner, createSession } = makeOmpPluginRunnerStub();
|
||||
|
||||
const result = await createResolvedAgentSession(sessionOptions({
|
||||
pluginRunner: pluginRunner as never,
|
||||
defaultProvider: undefined,
|
||||
defaultModelId: undefined,
|
||||
fallbackProvider: "omp-cli",
|
||||
fallbackModelId: "omp-cli/MiniMax-M2.5",
|
||||
fallbackThinkingLevel: "high",
|
||||
}));
|
||||
|
||||
expect(result.runtimeId).toBe("omp");
|
||||
expect(createSession).toHaveBeenCalledWith(expect.objectContaining({
|
||||
defaultProvider: "omp-cli",
|
||||
defaultModelId: "MiniMax-M2.5",
|
||||
defaultThinkingLevel: "high",
|
||||
fallbackProvider: undefined,
|
||||
fallbackModelId: undefined,
|
||||
fallbackThinkingLevel: undefined,
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([
|
||||
["an absent runtime registration", makeOmpPluginRunnerStub({ includeOmp: false }).pluginRunner],
|
||||
["no pluginRunner", undefined],
|
||||
])("reports OMP plugin remediation for %s", async (_label, pluginRunner) => {
|
||||
const promise = createResolvedAgentSession(sessionOptions({
|
||||
pluginRunner: pluginRunner as never,
|
||||
defaultProvider: "omp-cli",
|
||||
defaultModelId: "MiniMax-M2.5",
|
||||
}));
|
||||
|
||||
await expect(promise).rejects.toThrow(/OMP runtime plugin/);
|
||||
await expect(promise).rejects.not.toThrow(/not found in the pi model registry/);
|
||||
});
|
||||
|
||||
it("reports OMP plugin remediation for an unavailable explicit omp hint", async () => {
|
||||
const { pluginRunner } = makeOmpPluginRunnerStub({ includeOmp: false });
|
||||
|
||||
await expect(createResolvedAgentSession(sessionOptions({
|
||||
pluginRunner: pluginRunner as never,
|
||||
runtimeHint: "omp",
|
||||
defaultProvider: "omp-cli",
|
||||
defaultModelId: "MiniMax-M2.5",
|
||||
}))).rejects.toThrow(/OMP runtime plugin/);
|
||||
});
|
||||
|
||||
it("uses mock without OMP lookup in test mode or when mock is primary", async () => {
|
||||
const testModeRunner = makeOmpPluginRunnerStub();
|
||||
const testModeResult = await createResolvedAgentSession(sessionOptions({
|
||||
pluginRunner: testModeRunner.pluginRunner as never,
|
||||
settings: { testMode: true } as never,
|
||||
defaultProvider: "omp-cli",
|
||||
defaultModelId: "MiniMax-M2.5",
|
||||
}));
|
||||
expect(testModeResult.runtimeId).toBe(MOCK_PROVIDER_ID);
|
||||
expect(testModeRunner.getRuntimeById).not.toHaveBeenCalled();
|
||||
|
||||
const mockRunner = makeOmpPluginRunnerStub();
|
||||
const mockResult = await createResolvedAgentSession(sessionOptions({
|
||||
pluginRunner: mockRunner.pluginRunner as never,
|
||||
defaultProvider: MOCK_PROVIDER_ID,
|
||||
defaultModelId: "scripted",
|
||||
fallbackProvider: "omp-cli",
|
||||
fallbackModelId: "MiniMax-M2.5",
|
||||
}));
|
||||
expect(mockResult.runtimeId).toBe(MOCK_PROVIDER_ID);
|
||||
expect(mockRunner.getRuntimeById).not.toHaveBeenCalled();
|
||||
|
||||
const configuredMockRunner = makeOmpPluginRunnerStub();
|
||||
const configuredMockResult = await createResolvedAgentSession(sessionOptions({
|
||||
pluginRunner: configuredMockRunner.pluginRunner as never,
|
||||
settings: { defaultProvider: MOCK_PROVIDER_ID } as never,
|
||||
defaultProvider: "omp-cli",
|
||||
defaultModelId: "MiniMax-M2.5",
|
||||
}));
|
||||
expect(configuredMockResult.runtimeId).toBe(MOCK_PROVIDER_ID);
|
||||
expect(configuredMockRunner.getRuntimeById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("respects a non-OMP explicit runtime hint and leaves Grok routing independent", async () => {
|
||||
const { pluginRunner, getRuntimeById } = makeOmpPluginRunnerStub({ includeOther: true });
|
||||
const explicitResult = await createResolvedAgentSession(sessionOptions({
|
||||
pluginRunner: pluginRunner as never,
|
||||
runtimeHint: "other",
|
||||
defaultProvider: "omp-cli",
|
||||
defaultModelId: "MiniMax-M2.5",
|
||||
}));
|
||||
expect(explicitResult.runtimeId).toBe("other");
|
||||
expect(getRuntimeById).not.toHaveBeenCalledWith("omp");
|
||||
|
||||
vi.spyOn(fusionCore, "isGrokApiKeyFusionVisible").mockReturnValue(true);
|
||||
const grokRunner = makeOmpPluginRunnerStub();
|
||||
await createResolvedAgentSession(sessionOptions({
|
||||
pluginRunner: grokRunner.pluginRunner as never,
|
||||
defaultProvider: "grok-cli",
|
||||
defaultModelId: "grok-4.5",
|
||||
}));
|
||||
expect(grokRunner.getRuntimeById).not.toHaveBeenCalledWith("omp");
|
||||
});
|
||||
});
|
||||
@@ -355,6 +355,69 @@ function stripGrokCliModelProviderPrefix(modelId: string | undefined): string |
|
||||
: normalized;
|
||||
}
|
||||
|
||||
const OMP_CLI_PROVIDER_ID = "omp-cli";
|
||||
|
||||
function isOmpCliSelection(runtimeOptions: AgentRuntimeOptions): boolean {
|
||||
return runtimeOptions.defaultProvider === OMP_CLI_PROVIDER_ID
|
||||
|| runtimeOptions.fallbackProvider === OMP_CLI_PROVIDER_ID;
|
||||
}
|
||||
|
||||
function stripOmpCliModelProviderPrefix(modelId: string | undefined): string | undefined {
|
||||
const normalized = modelId?.trim();
|
||||
if (!normalized) return normalized;
|
||||
const ompCliPrefix = `${OMP_CLI_PROVIDER_ID}/`;
|
||||
return normalized.startsWith(ompCliPrefix)
|
||||
? normalized.slice(ompCliPrefix.length)
|
||||
: normalized;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:OmpAcp 2026-07-18-09:00:
|
||||
FN-8262: `omp-cli/*` models are dynamically discovered from `omp models` and are never registered in pi's execution registry, so route primary and fallback selections to the bundled `omp` ACP runtime before pi resolves a model. Test mode must short-circuit to mock without looking up OMP; an unavailable explicit `runtimeHint: "omp"` must report the OMP plugin remediation rather than pi's misleading model-not-found error.
|
||||
*/
|
||||
function buildMissingOmpRuntimeError(): Error {
|
||||
return new Error(
|
||||
"Oh My Pi (omp) models require the bundled OMP runtime plugin. "
|
||||
+ "Install and enable the OMP Runtime plugin (fusion-plugin-omp-runtime) and ensure the `omp` binary is installed and authenticated (`omp acp`, credentials under ~/.omp).",
|
||||
);
|
||||
}
|
||||
|
||||
function deriveOmpRuntimeHint(
|
||||
runtimeOptions: AgentRuntimeOptions,
|
||||
pluginRunner: PluginRunner | undefined,
|
||||
): string | undefined {
|
||||
if (!isOmpCliSelection(runtimeOptions)) return undefined;
|
||||
try {
|
||||
if (pluginRunner?.getRuntimeById("omp")) return "omp";
|
||||
} catch {
|
||||
throw buildMissingOmpRuntimeError();
|
||||
}
|
||||
throw buildMissingOmpRuntimeError();
|
||||
}
|
||||
|
||||
function applyOmpCliRuntimeOptions(runtimeOptions: AgentRuntimeOptions): AgentRuntimeOptions {
|
||||
if (runtimeOptions.defaultProvider === OMP_CLI_PROVIDER_ID) {
|
||||
return {
|
||||
...runtimeOptions,
|
||||
defaultModelId: stripOmpCliModelProviderPrefix(runtimeOptions.defaultModelId),
|
||||
};
|
||||
}
|
||||
|
||||
if (runtimeOptions.fallbackProvider === OMP_CLI_PROVIDER_ID) {
|
||||
return {
|
||||
...runtimeOptions,
|
||||
defaultProvider: runtimeOptions.fallbackProvider,
|
||||
defaultModelId: stripOmpCliModelProviderPrefix(runtimeOptions.fallbackModelId),
|
||||
defaultThinkingLevel: runtimeOptions.fallbackThinkingLevel ?? runtimeOptions.defaultThinkingLevel,
|
||||
fallbackProvider: undefined,
|
||||
fallbackModelId: undefined,
|
||||
fallbackThinkingLevel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return runtimeOptions;
|
||||
}
|
||||
|
||||
function buildMissingGrokRuntimeError(): Error {
|
||||
return new Error(
|
||||
"Grok CLI models require the bundled Grok CLI runtime when no Fusion-visible GROK_API_KEY is set. "
|
||||
@@ -646,7 +709,9 @@ export async function createResolvedAgentSession(
|
||||
// FNXC:McpConfig 2026-06-25-22:06:
|
||||
// createResolvedAgentSession is the common lane helper for executor, reviewer, validator, workflow model-node, summarization, and merger-adjacent paths that pass MCP through this seam. Preserve `mcpServers` verbatim here; runtime-resolution/pi own support-gated forwarding and content-free skip logging.
|
||||
|
||||
const useMockRuntime = isMockProviderId(runtimeOptions.defaultProvider);
|
||||
const testModeActive = settings ? isTestModeActive(settings) : false;
|
||||
const mockProviderActive = isMockProviderId(runtimeOptions.defaultProvider);
|
||||
const useMockRuntime = mockProviderActive || testModeActive;
|
||||
const effectiveRuntimeOptions = useMockRuntime
|
||||
? {
|
||||
...runtimeOptions,
|
||||
@@ -676,10 +741,21 @@ export async function createResolvedAgentSession(
|
||||
const autoGrokRuntimeHint = !useMockRuntime && !runtimeHint
|
||||
? deriveGrokRuntimeHintForNoVisibleKey(runtimeOptions, pluginRunner)
|
||||
: undefined;
|
||||
const effectiveRuntimeHint = autoGrokRuntimeHint ?? runtimeHint;
|
||||
const autoOmpRuntimeHint = !useMockRuntime && !runtimeHint
|
||||
? deriveOmpRuntimeHint(runtimeOptions, pluginRunner)
|
||||
: undefined;
|
||||
const effectiveRuntimeHint = autoGrokRuntimeHint ?? autoOmpRuntimeHint ?? runtimeHint;
|
||||
const usesOmpRuntime = effectiveRuntimeHint === "omp" && isOmpCliSelection(runtimeOptions);
|
||||
if (usesOmpRuntime) {
|
||||
// resolveRuntime intentionally falls back to pi for an unavailable hint; OMP
|
||||
// selections must fail here instead so pi never attempts registry resolution.
|
||||
deriveOmpRuntimeHint(runtimeOptions, pluginRunner);
|
||||
}
|
||||
const effectiveRuntimeOptionsWithModel: AgentRuntimeOptions = autoGrokRuntimeHint
|
||||
? applyGrokCliNoKeyRuntimeOptions(effectiveRuntimeOptions)
|
||||
: effectiveRuntimeOptions;
|
||||
: usesOmpRuntime
|
||||
? applyOmpCliRuntimeOptions(effectiveRuntimeOptions)
|
||||
: effectiveRuntimeOptions;
|
||||
|
||||
const resolved = useMockRuntime
|
||||
? {
|
||||
@@ -722,8 +798,6 @@ export async function createResolvedAgentSession(
|
||||
};
|
||||
const result = await resolved.runtime.createSession(sessionCreateOptions);
|
||||
|
||||
const testModeActive = settings ? isTestModeActive(settings) : false;
|
||||
const mockProviderActive = isMockProviderId(runtimeOptions.defaultProvider);
|
||||
const noModelResolved = !mockProviderActive && !testModeActive && (!runtimeOptions.defaultProvider || !runtimeOptions.defaultModelId);
|
||||
const runtimeBuiltInFallbackModel = noModelResolved ? resolved.runtime.describeModel(result.session) : undefined;
|
||||
if (noModelResolved) {
|
||||
@@ -751,7 +825,8 @@ export async function createResolvedAgentSession(
|
||||
...(noModelResolved ? { noModelResolved: true, runtimeBuiltInFallbackModel } : {}),
|
||||
...(effectiveRuntimeHint ? { runtimeHint: effectiveRuntimeHint } : {}),
|
||||
...(autoGrokRuntimeHint ? { reason: "grok-cli-no-visible-key" } : {}),
|
||||
...(!autoGrokRuntimeHint && "fallbackReason" in resolved && resolved.fallbackReason ? { reason: resolved.fallbackReason } : {}),
|
||||
...(autoOmpRuntimeHint ? { reason: "omp-cli-runtime" } : {}),
|
||||
...(!autoGrokRuntimeHint && !autoOmpRuntimeHint && "fallbackReason" in resolved && resolved.fallbackReason ? { reason: resolved.fallbackReason } : {}),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user