Merge pull request #75 from patlux/fix/omp-compat-import

fix(core): resolve the compat api registration at runtime for Oh My Pi
This commit is contained in:
Patrick Wozniak
2026-09-02 10:17:31 +02:00
committed by GitHub
5 changed files with 81 additions and 10 deletions
+27
View File
@@ -34,6 +34,33 @@ jobs:
- run: npm ci - run: npm ci
- run: npm run format:check - run: npm run format:check
# ──────────────────────────────────────────────────────────
# Oh My Pi host compatibility — real omp binary, mock API
# ──────────────────────────────────────────────────────────
omp-compat:
name: Oh My Pi compatibility
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: npm ci
- name: Install Oh My Pi
run: |
npm install -g @oh-my-pi/pi-coding-agent@latest
echo "OMP_BIN=$(npm prefix -g)/bin/omp" >> "$GITHUB_ENV"
- name: Verify omp starts
run: '"$OMP_BIN" --version'
- name: Run OMP compatibility suite
env:
OMP_COMPAT_REQUIRED: "1"
run: node tests/test-omp-compat.mjs
# ────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────
# Code-level vulnerability scanning (SAST) # Code-level vulnerability scanning (SAST)
# ────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────
+3
View File
@@ -2,6 +2,9 @@
## Unreleased ## Unreleased
- Fix `omp plugin install` on Oh My Pi 18.x, which rejected 0.6.1 because its pi-ai lacks the `registerApiProvider` export; the compat registration now resolves at runtime and is skipped on hosts that register custom APIs themselves.
- Run the Oh My Pi compatibility suite against a real `omp` binary in CI as a required check, and assert there that the extension loads against OMP's bundled pi packages.
## 0.6.1 - 2026-09-01 ## 0.6.1 - 2026-09-01
- Expose selectable thinking levels (`minimal`, `low`, `medium`, `high`, `xhigh`) for `meta/muse-spark-1.1`, `meta/muse-spark-1.2`, and `meta/muse-spark-1.2-contributor` through a manual catalog override, so `/thinking` and `Shift+Tab` no longer stay locked on `off` for these reasoning models. - Expose selectable thinking levels (`minimal`, `low`, `medium`, `high`, `xhigh`) for `meta/muse-spark-1.1`, `meta/muse-spark-1.2`, and `meta/muse-spark-1.2-contributor` through a manual catalog override, so `/thinking` and `Shift+Tab` no longer stay locked on `off` for these reasoning models.
+11
View File
@@ -52,6 +52,17 @@ COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key npm run test:e2e:liv
Use `npm run test:e2e:live:all` with the Go and GOAT file variables to run both subscription transports sequentially. Store keys in a secret manager and export each one to a new mode-`0600` temporary file for the test; never add key files to the repository. Direct `*_API_KEY` variables are intended primarily for protected CI secrets. Use `npm run test:e2e:live:all` with the Go and GOAT file variables to run both subscription transports sequentially. Store keys in a secret manager and export each one to a new mode-`0600` temporary file for the test; never add key files to the repository. Direct `*_API_KEY` variables are intended primarily for protected CI secrets.
### Oh My Pi compatibility
`tests/test-omp-compat.mjs` runs the extension inside a real `omp` binary against a mock Command Code API. It skips locally when `omp` is not on `PATH`; CI installs Oh My Pi and runs it as a required check with `OMP_COMPAT_REQUIRED=1`, so a change that only loads on pi fails CI instead of the next `omp plugin install`.
To run it locally, point `OMP_BIN` at an omp executable (Oh My Pi needs Bun ≥ 1.3.14):
```sh
npm install -g @oh-my-pi/pi-coding-agent
OMP_BIN="$(npm prefix -g)/bin/omp" node tests/test-omp-compat.mjs
```
Before opening a PR, run: Before opening a PR, run:
```sh ```sh
+22 -10
View File
@@ -6,11 +6,8 @@
*/ */
import { AssistantMessageEventStream } from "@earendil-works/pi-ai" import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
import { import * as piAiCompat from "@earendil-works/pi-ai/compat"
registerApiProvider, import { streamSimple as streamNativeProvider } from "@earendil-works/pi-ai/compat"
streamSimple as streamNativeProvider,
type ApiStreamSimpleFunction,
} from "@earendil-works/pi-ai/compat"
import { import {
getAgentDir, getAgentDir,
type ExtensionAPI, type ExtensionAPI,
@@ -45,6 +42,24 @@ import { createCommandCodeTransportRouter } from "./src/transport.ts"
const COMMAND_CODE_API = "commandcode-custom" const COMMAND_CODE_API = "commandcode-custom"
const COMPAT_SOURCE_ID = "pi-commandcode-provider" const COMPAT_SOURCE_ID = "pi-commandcode-provider"
type CompatStreamFunction = (
model: Parameters<typeof streamNativeProvider>[0],
context: Parameters<typeof streamNativeProvider>[1],
options?: Parameters<typeof streamNativeProvider>[2],
) => AssistantMessageEventStream
/**
* pi's compat entrypoint exposes `registerApiProvider`; Oh My Pi maps
* `@earendil-works/pi-ai/compat` onto its own pi-ai, which lacks that export
* and registers custom APIs itself inside `registerProvider`. Resolve the
* function at runtime so the extension loads on both hosts.
*/
function registerCompatApiProvider(stream: CompatStreamFunction): void {
const register = (piAiCompat as { registerApiProvider?: unknown }).registerApiProvider
if (typeof register !== "function") return
register({ api: COMMAND_CODE_API, stream, streamSimple: stream }, COMPAT_SOURCE_ID)
}
function commandCodeHeaders(): Record<string, string> | undefined { function commandCodeHeaders(): Record<string, string> | undefined {
if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") { if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
return { "x-cmd-zdr": "1" } return { "x-cmd-zdr": "1" }
@@ -135,16 +150,13 @@ export default async function (pi: ExtensionAPI) {
// custom api there so those calls reach the same transport. The registry // custom api there so those calls reach the same transport. The registry
// resolves no credentials for extension providers, so fall back to the // resolves no credentials for extension providers, so fall back to the
// configured key when the caller passes none. // configured key when the caller passes none.
const compatStream: ApiStreamSimpleFunction = (model, context, options) => const compatStream: CompatStreamFunction = (model, context, options) =>
transport.stream( transport.stream(
model, model,
context, context,
options?.apiKey ? options : { ...options, apiKey: getConfiguredApiKey() }, options?.apiKey ? options : { ...options, apiKey: getConfiguredApiKey() },
) as AssistantMessageEventStream ) as AssistantMessageEventStream
registerApiProvider( registerCompatApiProvider(compatStream)
{ api: COMMAND_CODE_API, stream: compatStream, streamSimple: compatStream },
COMPAT_SOURCE_ID,
)
pi.on("message_end", async (event, ctx) => { pi.on("message_end", async (event, ctx) => {
if (event.message.role !== "assistant") return if (event.message.role !== "assistant") return
+18
View File
@@ -40,6 +40,10 @@ function findOmpBinary() {
const OMP_BIN = findOmpBinary() const OMP_BIN = findOmpBinary()
if (!OMP_BIN) { if (!OMP_BIN) {
if (process.env.OMP_COMPAT_REQUIRED === "1") {
console.error("[omp-compat] FAIL - omp is required but not on PATH and OMP_BIN is unset")
process.exit(1)
}
console.log("[omp-compat] SKIP - omp is not on PATH") console.log("[omp-compat] SKIP - omp is not on PATH")
process.exit(0) process.exit(0)
} }
@@ -211,6 +215,20 @@ function runOmp(args, timeoutMs = 30_000) {
} }
try { try {
console.log("[omp-compat] extension loads against the host's pi packages")
// OMP remaps `@earendil-works/pi-ai/compat` onto its own pi-ai and rejects
// any named import that module does not export, both in `omp plugin
// install` validation and when loading the extension. 0.6.1 shipped such an
// import and could not be installed on omp 18 (#74). `omp models -e EXT`
// runs the same loader, so it reproduces that failure without a registry.
const load = await runOmp(["models", "-e", EXT_PATH])
assert.equal(load.code, 0, load.stderr)
assert.doesNotMatch(
load.stdout + load.stderr,
/Failed to load extension|not found in module/,
"the extension must only import what OMP's bundled pi packages export",
)
console.log("[omp-compat] list models through real extension") console.log("[omp-compat] list models through real extension")
modelListRequestCount = 0 modelListRequestCount = 0
// Prefer the flag form `omp -e EXT --list-models`; Homebrew's `omp` // Prefer the flag form `omp -e EXT --list-models`; Homebrew's `omp`