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 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)
|
||||||
# ──────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
Reference in New Issue
Block a user