Files
fusion/packages/dashboard/src/omp-model-cache.ts
gsxdsm b563b12662 feat: add Oh My Pi (omp) ACP runtime plugin (#2083)
## Summary

- Add `fusion-plugin-omp-runtime` so Fusion agents can run through
operator-installed **Oh My Pi (`omp`)** over the [Agent Client
Protocol](https://omp.sh/docs/acp) (`omp acp`).
- Wire staged/bundled install, Settings → Authentication card (enable +
binary path), model discovery (`omp models` → `omp-cli/*`), and MCP
eligibility for runtime id `omp`.
- Forward Fusion `systemPrompt` via ACP `session/new`
`_meta.systemPromptOverride`.

## How operators use it

1. Install/auth `omp` (credentials under `~/.omp`).
2. Enable **Oh My Pi — via omp ACP** in Settings → Authentication
(optional binary path).
3. Set agent **Runtime Source → OMP Runtime** (`runtimeHint: "omp"`), or
pick an `omp-cli/*` model when enabled.

## Known v1 gaps

- No Grok-style Fusion `fn_*` loopback tool bridge yet (operator MCP is
forwarded; in-process custom tools are not).
- Model is fixed at spawn (`omp --model … acp`); no mid-session Fusion
model switch.

## Test plan

- [x] `pnpm --filter @fusion-plugin-examples/omp-runtime test` (unit +
live ACP when `omp` is on PATH)
- [x] Auth routes: `POST /api/auth/omp-cli`, `GET
/api/providers/omp-cli/status`
- [x] Engine `runtimeSupportsMcp("omp")`
- [ ] Manual: enable card in dashboard, select OMP runtime on an agent,
run a short chat turn

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Oh My Pi (OMP) CLI support as an ACP-backed runtime and model
provider, including model discovery and probing.
* Added dashboard auth/status controls to enable OMP, check readiness,
and configure the local binary path (with validation).
* Exposed OMP custom `fn_*` tools via an MCP loopback bridge, plus
optional filesystem capabilities and stricter tool permission gating.
* **Documentation**
* Added/expanded OMP runtime contract and integration docs (including
the ACP session/handshake flow).
* **Tests**
* Added Vitest coverage for settings wiring, provider status, model
discovery, runtime sessions, permissions, MCP bridging, and live
connectivity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-14 08:18:52 -07:00

100 lines
2.6 KiB
TypeScript

/**
* OMP CLI discovery → model-picker mapping, behind a short-TTL single-flight cache.
*
* FNXC:OmpAcp 2026-07-13-22:50:
* Mirrors grok-model-cache / cursor-model-cache. When useOmpCli is true, surface
* models from `omp models` under provider id `omp-cli`. Never throws; empty on failure.
*/
import { discoverOmpCliModels } from "./runtime-provider-probes.js";
export interface OmpPickerModel {
provider: "omp-cli";
id: string;
name: string;
reasoning: boolean;
contextWindow: number;
}
export const OMP_PICKER_PROVIDER_ID = "omp-cli" as const;
const DEFAULT_TTL_MS = 60_000;
const EMPTY_RESULT_TTL_MS = 5_000;
export function ompDiscoveryToModels(
models: ReadonlyArray<{ id: string; label?: string }>,
): OmpPickerModel[] {
const seen = new Set<string>();
const result: OmpPickerModel[] = [];
for (const model of models) {
const id = model.id?.trim();
if (!id || seen.has(id)) continue;
seen.add(id);
result.push({
provider: OMP_PICKER_PROVIDER_ID,
id,
name: model.label?.trim() || id,
reasoning: false,
contextWindow: 0,
});
}
return result;
}
interface CacheEntry {
fetchedAt: number;
models: OmpPickerModel[];
ttlMs: number;
}
const cache = new Map<string, CacheEntry>();
const inFlight = new Map<string, Promise<OmpPickerModel[]>>();
export function __resetOmpPickerModelsCacheForTests(): void {
cache.clear();
inFlight.clear();
}
export interface GetOmpPickerModelsOptions {
binaryPath?: string;
ttlMs?: number;
now?: () => number;
}
export async function getOmpPickerModels(
opts?: GetOmpPickerModelsOptions,
): Promise<OmpPickerModel[]> {
const binaryPath = opts?.binaryPath ?? "omp";
const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
const now = opts?.now ?? Date.now;
const nowMs = now();
const cached = cache.get(binaryPath);
if (cached && nowMs - cached.fetchedAt < cached.ttlMs) {
return cached.models;
}
const existingInFlight = inFlight.get(binaryPath);
if (existingInFlight) return existingInFlight;
const fetchPromise = (async (): Promise<OmpPickerModel[]> => {
try {
const result = await discoverOmpCliModels({ binaryPath });
if (!result || result.models.length === 0) return [];
return ompDiscoveryToModels(result.models);
} catch {
return [];
}
})();
inFlight.set(binaryPath, fetchPromise);
try {
const models = await fetchPromise;
const effectiveTtlMs = models.length === 0 ? EMPTY_RESULT_TTL_MS : ttlMs;
cache.set(binaryPath, { fetchedAt: now(), models, ttlMs: effectiveTtlMs });
return models;
} finally {
inFlight.delete(binaryPath);
}
}