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:
@@ -34,6 +34,33 @@ jobs:
|
||||
- run: npm ci
|
||||
- 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)
|
||||
# ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
## 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
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 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:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
*/
|
||||
|
||||
import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
|
||||
import {
|
||||
registerApiProvider,
|
||||
streamSimple as streamNativeProvider,
|
||||
type ApiStreamSimpleFunction,
|
||||
} from "@earendil-works/pi-ai/compat"
|
||||
import * as piAiCompat from "@earendil-works/pi-ai/compat"
|
||||
import { streamSimple as streamNativeProvider } from "@earendil-works/pi-ai/compat"
|
||||
import {
|
||||
getAgentDir,
|
||||
type ExtensionAPI,
|
||||
@@ -45,6 +42,24 @@ import { createCommandCodeTransportRouter } from "./src/transport.ts"
|
||||
const COMMAND_CODE_API = "commandcode-custom"
|
||||
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 {
|
||||
if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_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
|
||||
// resolves no credentials for extension providers, so fall back to the
|
||||
// configured key when the caller passes none.
|
||||
const compatStream: ApiStreamSimpleFunction = (model, context, options) =>
|
||||
const compatStream: CompatStreamFunction = (model, context, options) =>
|
||||
transport.stream(
|
||||
model,
|
||||
context,
|
||||
options?.apiKey ? options : { ...options, apiKey: getConfiguredApiKey() },
|
||||
) as AssistantMessageEventStream
|
||||
registerApiProvider(
|
||||
{ api: COMMAND_CODE_API, stream: compatStream, streamSimple: compatStream },
|
||||
COMPAT_SOURCE_ID,
|
||||
)
|
||||
registerCompatApiProvider(compatStream)
|
||||
|
||||
pi.on("message_end", async (event, ctx) => {
|
||||
if (event.message.role !== "assistant") return
|
||||
|
||||
@@ -40,6 +40,10 @@ function findOmpBinary() {
|
||||
|
||||
const OMP_BIN = findOmpBinary()
|
||||
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")
|
||||
process.exit(0)
|
||||
}
|
||||
@@ -211,6 +215,20 @@ function runOmp(args, timeoutMs = 30_000) {
|
||||
}
|
||||
|
||||
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")
|
||||
modelListRequestCount = 0
|
||||
// Prefer the flag form `omp -e EXT --list-models`; Homebrew's `omp`
|
||||
|
||||
Reference in New Issue
Block a user