diff --git a/.omo/run-continuation/ses_13df7fbe7ffeFEg7W8RPT6aaOD.json b/.omo/run-continuation/ses_13df7fbe7ffeFEg7W8RPT6aaOD.json new file mode 100644 index 000000000000..209722c38b7f --- /dev/null +++ b/.omo/run-continuation/ses_13df7fbe7ffeFEg7W8RPT6aaOD.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_13df7fbe7ffeFEg7W8RPT6aaOD", + "updatedAt": "2026-06-16T14:00:03.388Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-06-16T14:00:03.388Z" + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index ac154c6157e1..c081a84c1aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_Cycle open — entries are added here as PRs merge into `release/v3.8.27`._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index 5b8b543c624a..b8f80c7b87bd 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md index 5ba821284458..d6be17945052 100644 --- a/docs/i18n/az/CHANGELOG.md +++ b/docs/i18n/az/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index 5ba821284458..d6be17945052 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index 0db73346c0a2..56cdbfeeb1c7 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index 6e6ecbf61fd8..3339eed465f0 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 5aae04c2b382..1992c27ae690 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index 60453894769d..6d80745373ec 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index e4b50140a346..2fe24aea91fc 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index c9b7f1c1eb6b..2201c0fcff8c 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index e67c3b82d7eb..a90e48f993aa 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index a11461a7dddd..04622e09c3a2 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index b7e03639099d..3e9105e1b12b 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index df506e0a21e7..27ae953f9e25 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 3b9c5a200167..845eaad63563 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index b24157d80f42..20edec49b94f 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index 7420d2f9943b..f9563b3291c1 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index 99e1a40dd693..43a8bc58a306 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index d052bb3d0425..4c2951387c84 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index aec4d34bc2c3..9db0991052bc 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index 07081ef476b1..6306a6e9069f 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index 375a9004c9d7..92fd0993dfac 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 764739a7a1a7..4c58d57105da 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index cdad222e1b75..39cfce681d21 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 540758225ab7..defa53e1c1f9 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index 934b4ab5aefd..1ae7e4d50057 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index 842fdf33eea6..91f1405df8ea 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index c11b13850cf9..1116092d952a 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index 5c16ce49b4e0..c7798402d380 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index 543e96200293..fac0658ed6ef 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index bfbf32e71837..ee01642c283a 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index d5993cd760bd..f04e81154f62 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index a91421234ad2..5c56f70ac53b 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index 9df3b002c3b4..7608d3452d8d 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index cfb4e9273a1c..96964f36efee 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index 99d60804ef16..585db1d9cd0f 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index e9d4d7ad2fd3..4df1694ec86e 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index 573c4e94de6d..4cf98b63ddbd 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index 687e84c30ad3..c06d2634d2fe 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index 81878e2c0b78..cd075238609d 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index 7845c11a6f07..2ebe300a2ccc 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index 9da0b2dda0e1..cc15d7b0ea82 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/plugins/PLUGIN_DEVELOPMENT.md b/docs/plugins/PLUGIN_DEVELOPMENT.md new file mode 100644 index 000000000000..903bb31d3f9a --- /dev/null +++ b/docs/plugins/PLUGIN_DEVELOPMENT.md @@ -0,0 +1,575 @@ +--- +title: "Plugin Development Guide" +version: 3.8.16 +lastUpdated: 2026-06-08 +--- + +# Plugin Development Guide + +> **TL;DR**: This guide covers the **day-to-day workflow** for building, testing, signing, and debugging OmniRoute plugins. For the SDK API surface, see the [Plugin SDK Reference](./PLUGIN_SDK.md). For the marketplace, see [Plugin Marketplace](./PLUGIN_MARKETPLACE.md). + +--- + +## Overview + +OmniRoute's plugin system has **two layers** that work together: + +| Layer | Purpose | Where it runs | +| --------------- | ---------------------------------------- | ---------------------------------------------- | +| **SDK plugins** | Hook-based request/response interception | In-process sandboxed VM (the OmniRoute server) | +| **CLI plugins** | Command-based CLI extensions | Separate Node.js process (the `omniroute` CLI) | + +This guide is about **SDK plugins** — the in-process kind that hook into every request. For CLI plugins, see [CLI Plugin System](#cli-plugin-system) below or [`docs/dev/plugins.md`](../dev/plugins.md). + +--- + +## Lifecycle: Install → Activate → Run → Deactivate → Uninstall + +Every plugin goes through a **5-stage lifecycle** managed by `PluginManager` (`src/lib/plugins/manager.ts`): + +``` + ┌─────────────┐ + │ Installed │ ← files on disk + manifest valid + └──────┬──────┘ + │ activate() + ▼ + ┌─────────────┐ + ┌──────────▶│ Activated │ ← hooks registered, can intercept requests + │ └──────┬──────┘ + │ deactivate() │ (running) + │ ▼ + │ ┌─────────────┐ + │ │ Hooks fire │ ← onRequest, onResponse, onError per request + │ └──────┬──────┘ + │ │ + │ deactivate() │ + └──────────────────┘ + │ uninstall() + ▼ + ┌─────────────┐ + │ Removed │ ← files deleted, DB row removed + └─────────────┘ +``` + +### Built-in Events + +Plugins can register handlers for the following built-in events that cover the full request lifecycle, routing, rate limiting, and error handling. See [PLUGIN_SDK.md](./PLUGIN_SDK.md#built-in-events) for the complete list of events and their payloads. + +The core request-handling events are: + +```ts +import { definePlugin } from "omniroute/plugins/sdk"; + +export default definePlugin({ + name: "my-plugin", + + // Core request-handling events (always available) + onRequest: async (ctx) => { + console.log(`Request: ${ctx.requestId}`); + }, + + onResponse: async (ctx, response) => { + console.log(`Response for: ${ctx.requestId}`); + return response; // optionally transform + }, + + onError: async (ctx, error) => { + console.error(`Error: ${error.message}`); + }, + + // Optional routing events + onModelSelect: async (ctx) => { + /* ... */ + }, + onComboResolve: async (ctx) => { + /* ... */ + }, + onRateLimit: async (ctx) => { + /* ... */ + }, + onQuotaExhaust: async (ctx) => { + /* ... */ + }, + onProviderError: async (ctx) => { + /* ... */ + }, + + // Stream events + onStreamStart: async (ctx) => { + /* ... */ + }, + onStreamEnd: async (ctx) => { + /* ... */ + }, +}); +``` + +For the complete list of built-in events and their signatures, refer to `src/lib/plugins/hooks.ts` (BUILTIN_EVENTS constant). + +> **Note:** The `BUILTIN_EVENTS` list also includes **lifecycle events** (`onInstall`, `onActivate`, `onDeactivate`, `onUninstall`). These are fired internally by `PluginManager` during state transitions and are declared in the manifest's `hooks` field — they are not registerable via `definePlugin()`. + +--- + +## Dev Mode: Hot Reload on File Changes + +**`src/lib/plugins/devMode.ts`** exports `startDevMode(pluginDir, reloadFn)` and `stopDevMode()` for hot-reloading plugins when their files change on disk. The watcher is debounced at 500ms per source change. + +> **⚠️ Not yet wired up.** `startDevMode()` is exported but has **no call sites** (`grep -rn "startDevMode" src/ open-sse/` returns only the definition). The environment variables `PLUGIN_DEV_MODE` and `OMNIROUTE_PLUGIN_DEV` are not read anywhere — `PLUGIN_DEV_MODE` is only the **logger name** in `src/lib/plugins/devMode.ts:12`. Dev mode is therefore only invokable programmatically today; an env-var/CLI path that turns it on is planned but not implemented. + +### Programmatic invocation + +The function is exported and unit-testable, but in current `release/v3.8.20` no caller invokes it. To wire it up, the caller would do: + +```ts +// Example (not currently invoked anywhere in the codebase) +import { startDevMode } from "omniroute/plugins/devMode"; +import { reloadPlugin } from "./reload-helper"; + +startDevMode(getPluginDir(), async (pluginName) => { + await reloadPlugin(pluginName); +}); +``` + +### How It Works + +```ts +// src/lib/plugins/devMode.ts +const DEBOUNCE_MS = 500; + +export function startDevMode(pluginDir: string, reloadFn: ReloadFn): void { + if (watcher) return; + + watcher = watch(pluginDir, { recursive: true }, (_eventType, filename) => { + if (!filename) return; + const pluginName = filename.replace(/\\/g, "/").split("/")[0]; + if (!pluginName || pluginName.startsWith(".")) return; + + // Debounce rapid changes + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(async () => { + log.info("devMode.file_changed", { pluginName, file: filename }); + try { + await reloadFn(pluginName); + log.info("devMode.reloaded", { pluginName }); + } catch (err: unknown) { + log.error("devMode.reload_failed", { + pluginName, + error: err instanceof Error ? err.message : String(err), + }); + } + }, DEBOUNCE_MS); + }); + + log.info("devMode.started", { pluginDir }); +} +``` + +### Dev Mode Tips (when wired up) + +- **Edit `plugin.json`**: changes would be picked up on the next reload cycle. The manifest is re-validated each time. +- **Edit `index.js`**: same — the plugin is unloaded and re-loaded. +- **Multiple rapid saves**: the 500ms debounce means OmniRoute won't thrash during a flurry of saves. +- **Errors during reload**: the old version remains active. Check the server log (`PLUGIN_DEV_MODE` and `PLUGIN_MANAGER` loggers) for the error. + +### When NOT to Use Dev Mode + +- **Production**: dev mode is not yet invoked anywhere, so this is moot until the env-var/CLI wiring lands. When it is wired up, never enable it in production — the watcher adds CPU overhead and may briefly disable plugins during reloads. +- **Plugin uses native modules**: native bindings can't be re-initialized safely. Restart the server. + +--- + +## Testing Plugins + +**`src/lib/plugins/testRunner.ts`** provides a quick way to verify that all your plugin's hooks work without standing up a full request flow. + +### Quick Test + +```ts +import { testPlugin } from "omniroute/plugins/testRunner"; +import { validateManifest } from "omniroute/plugins/manifest"; +import { readFileSync } from "fs"; + +const raw = JSON.parse(readFileSync("./plugin.json", "utf-8")); +const manifest = validateManifest(raw); +const results = await testPlugin("./index.js", manifest); + +for (const r of results) { + console.log(`${r.passed ? "✓" : "✗"} ${r.hook} (${r.durationMs}ms)`); + if (!r.passed) console.error(` Error: ${r.error}`); +} +``` + +### What Gets Tested + +The test runner invokes **every registered hook** with a mock context: + +```ts +// src/lib/plugins/testRunner.ts +const MOCK_CONTEXT: PluginContext = { + requestId: "test-req-001", + body: { model: "gpt-4", messages: [{ role: "user", content: "test" }] }, + model: "gpt-4", + provider: "openai", + metadata: { test: true }, +}; +``` + +For each hook, the runner records: + +- `passed` (boolean): did the hook complete without throwing? +- `durationMs` (number): how long the hook took +- `output` (unknown): the hook's return value +- `error` (string): if the hook threw, the error message + +### Sample Output + +```json +[ + { "hook": "onRequest", "passed": true, "durationMs": 3, "output": null }, + { "hook": "onResponse", "passed": true, "durationMs": 1, "output": { "choices": [...] } }, + { "hook": "onError", "passed": true, "durationMs": 0, "output": null } +] +``` + +### Limitations + +The test runner uses a **fixed mock context** — it cannot simulate: + +- Streaming responses +- Multi-turn conversations +- Specific provider error formats +- Custom `PluginContext` shapes + +For full integration testing, use the [OmniRoute test harness](#integration-testing) in your own test suite. + +--- + +## Plugin Doctor: Diagnostic Health Checks + +**`src/lib/plugins/doctor.ts`** runs **5 health checks** on a plugin to help you diagnose common issues. + +### Running the Doctor + +The plugin doctor is available as a programmatic API only: + +```ts +// Programmatic API +import { runPluginDoctor } from "omniroute/plugins/doctor"; + +const result = await runPluginDoctor("~/.omniroute/plugins/my-plugin", "my-plugin"); +console.log(result); +``` + +Note: There is no CLI command for the doctor. To diagnose plugins, integrate `runPluginDoctor` into your plugin management tooling. + +### The 5 Checks + +| # | Check name | What it verifies | Status values | +| --- | -------------------- | ----------------------------------------- | ------------------------ | +| 1 | `directory_exists` | Plugin directory is on disk | `pass` / `fail` | +| 2 | `manifest_valid` | `plugin.json` parses and matches schema | `pass` / `fail` | +| 3 | `entry_point_exists` | `manifest.main` file is on disk | `pass` / `fail` / `warn` | +| 4 | `can_spawn` | Entry point has `.js` or `.mjs` extension | `pass` / `warn` | +| 5 | `db_status_correct` | Plugin row in DB matches filesystem state | `pass` / `warn` | + +### Interpreting Results + +```ts +interface DoctorResult { + pluginName: string; + checks: DoctorCheck[]; + overall: "healthy" | "degraded" | "unhealthy"; +} +``` + +- **`healthy`** — all 5 checks passed +- **`degraded`** — some `warn` (e.g. manifest valid but entry point missing) +- **`unhealthy`** — any `fail` (e.g. manifest invalid) + +### Common Issues and Fixes + +| Symptom | Likely cause | Fix | +| -------------------------- | --------------------------------------------- | ---------------------------------------------------------------- | +| `directory_exists: fail` | Plugin was deleted from disk but still in DB | Run `omniroute plugin uninstall ` | +| `manifest_valid: fail` | `plugin.json` doesn't match the schema | Check the [manifest schema](./PLUGIN_SDK.md#manifest-pluginjson) | +| `entry_point_exists: fail` | `manifest.main` points to a non-existent file | Fix the `main` field in `plugin.json` | +| `can_spawn: warn` | Entry point has `.ts` or other extension | Compile to `.js` or `.mjs` first | +| `db_status_correct: warn` | Plugin not in DB (was just installed) | Re-run `omniroute plugin install ` | + +--- + +## Plugin Signing & Verification + +**`src/lib/plugins/signing.ts`** provides Ed25519-based code signing so users can verify that a plugin came from a trusted source. + +### What's Available + +```ts +// SHA-256 hashing +sha256(data: Buffer): string +verifySha256(data: Buffer, expectedHash: string): boolean + +// Ed25519 signature verification +verifyEd25519(data: Buffer, signature: Buffer, publicKeyDer: Buffer): boolean +``` + +### Why Sign Plugins? + +- **Trust**: Users can verify a plugin was published by a specific author +- **Integrity**: Detect if a plugin package was tampered with after publishing +- **Supply chain**: Build a chain of trust from author → registry → user + +### Verifying a Plugin (v3.8.16+) + +```ts +import { verifyEd25519, verifySha256 } from "omniroute/plugins/signing"; +import { readFileSync } from "fs"; + +const pluginBytes = readFileSync("./my-plugin.tar.gz"); +const signature = readFileSync("./my-plugin.sig"); +const publicKey = readFileSync("./author-pubkey.der"); + +if (verifyEd25519(pluginBytes, signature, publicKey)) { + console.log("✓ Plugin signature valid"); +} else { + throw new Error("Plugin signature invalid — do not install"); +} +``` + +### Generating a Signature (Author Side) + +```ts +import { createPrivateKey, sign, generateKeyPairSync } from "crypto"; +import { readFileSync, writeFileSync } from "fs"; + +// 1. Generate a key pair (one-time) +const { publicKey, privateKey } = generateKeyPairSync("ed25519"); +writeFileSync("./author-pubkey.der", publicKey.export({ format: "der", type: "spki" })); +writeFileSync("./author-privkey.der", privateKey.export({ format: "der", type: "pkcs8" })); + +// 2. Sign your plugin package +const data = readFileSync("./my-plugin.tar.gz"); +const key = createPrivateKey(readFileSync("./author-privkey.der")); +const signature = sign(null, data, key); +writeFileSync("./my-plugin.sig", signature); +``` + +### Registry Workflow (Recommended) + +``` + Author Registry User + │ │ │ + │ 1. Generate key pair │ │ + │ 2. Publish pubkey.der ───────────▶│ │ + │ 3. Publish plugin.tar.gz ────────▶│ Store pubkey │ + │ 4. Publish plugin.sig ───────────▶│ Verify signature │ + │ │ │ + │ │ 5. User requests install ◀───│ + │ │ 6. Ship plugin + signature │ + │ │ ────────────────────────────▶│ + │ │ │ 7. Verify signature +``` + +> **Note**: The remote registry with signature verification is planned for **Phase 2** of the marketplace. For now, signing is **advisory** — OmniRoute does not block installation of unsigned plugins, but it surfaces a warning. + +--- + +## CLI Plugin System + +OmniRoute's **CLI** is also extensible. CLI plugins add new subcommands to the `omniroute` binary, similar to `gh extension` or `kubectl plugin`. + +> For the full CLI plugin reference, see [`docs/dev/plugins.md`](../dev/plugins.md). Summary below. + +### Quick Example + +```bash +# Install a CLI plugin from npm +omniroute plugin install stripe + +# Install a local plugin in development +omniroute plugin install ./my-plugin + +# Scaffold a new plugin +omniroute plugin scaffold myplugin +cd omniroute-cmd-myplugin +omniroute plugin install . +``` + +### CLI Plugin Package Layout + +``` +omniroute-cmd-myplugin/ +├── package.json # name: "omniroute-cmd-", type: "module", main: "index.mjs" +├── index.mjs # exports register(program, ctx) + optional meta +└── README.md +``` + +### CLI Plugin Entry Point + +```js +export const meta = { + name: "myplugin", + version: "0.1.0", + description: "My plugin for OmniRoute", + omnirouteApi: ">=4.0.0", +}; + +export function register(program, ctx) { + program + .command("myplugin") + .description(meta.description) + .option("-n, --name ") + .action(async (opts, cmd) => { + const gOpts = cmd.optsWithGlobals(); + const res = await ctx.apiFetch("/api/combos", { + baseUrl: gOpts.baseUrl, + apiKey: gOpts.apiKey, + }); + const data = await res.json(); + ctx.emit(data, gOpts); + }); +} +``` + +### CLI Plugin Context API + +| Property | Type | Description | +| ---------------------------- | ---------------- | ---------------------------------------------------------- | +| `ctx.apiFetch(path, opts)` | `async function` | Authenticated fetch to the OmniRoute server | +| `ctx.emit(data, opts)` | `function` | Output in `table`/`json`/`jsonl`/`csv` per `--output` flag | +| `ctx.t(key)` | `async function` | i18n translation lookup | +| `ctx.withSpinner(label, fn)` | `async function` | Wraps async fn with ora spinner | +| `ctx.baseUrl` | `string` | Resolved base URL | +| `ctx.apiKey` | `string \| null` | API key if provided | + +### CLI Plugin Discovery + +Plugins are discovered from: + +1. `~/.omniroute/plugins//` — user-local installs +2. `OMNIROUTE_PLUGIN_PATH` env var — custom directory + +Loading errors are caught and printed as warnings — a broken plugin never crashes the CLI. + +### Security Warning + +CLI plugins run with the **same Node.js process privileges** as `omniroute`. Only install plugins from sources you trust. `omniroute plugin install` shows an explicit warning and requires `--yes` or interactive confirmation. + +--- + +## Best Practices + +### Plugin Structure + +``` +my-plugin/ +├── plugin.json # Manifest (required) +├── index.js # Entry point (or index.mjs, index.ts compiled) +├── README.md # User-facing docs +├── CHANGELOG.md # Version history +├── lib/ # Internal modules +│ ├── helpers.js +│ └── config.js +├── test/ # Tests +│ ├── plugin.test.js +│ └── mocks.js +└── examples/ # Usage examples + └── basic.js +``` + +### Versioning + +- Follow [SemVer](https://semver.org/): `MAJOR.MINOR.PATCH` +- Bump `MAJOR` for breaking API changes +- Bump `MINOR` for new features (backward compatible) +- Bump `PATCH` for bug fixes +- Update `CHANGELOG.md` for every release + +### Error Handling + +Always handle errors gracefully — a thrown exception in a hook will block the request: + +```ts +import { definePlugin, addMetadata } from "omniroute/plugins/sdk"; + +export default definePlugin({ + name: "my-plugin", + onRequest: async (ctx) => { + try { + const result = await someExternalCall(ctx); + return addMetadata({ external: result }); + } catch (err) { + // Log but don't fail the request + console.error("External call failed:", err); + return addMetadata({ external: null, error: String(err) }); + } + }, +}); +``` + +### Performance + +- Keep hooks **fast** — they run on every request +- For expensive operations, use **caching** with TTL +- Use `Promise.all` for independent async work +- Avoid `setTimeout` / `setInterval` inside hooks + +### Permissions + +Request **only the permissions you need**: + +```json +{ + "requires": { + "permissions": ["network"] + } +} +``` + +Plugins without `network` permission cannot call `fetch` — the global is simply not available in the sandbox. + +--- + +## Troubleshooting + +### "Plugin not loading" + +Use the plugin doctor (programmatically) to diagnose: + +```ts +import { runPluginDoctor } from "omniroute/plugins/doctor"; + +const result = await runPluginDoctor("~/.omniroute/plugins/my-plugin", "my-plugin"); +console.log(result); +``` + +Check the 5 checks. The most common cause is `manifest_valid: fail` — usually a missing required field. + +### "Hook not firing" + +1. Check the plugin is **activated** (not just installed): `omniroute plugin list` +2. Check the **event** in your manifest: `onRequest: { enabled: true, priority: 50 }` +3. Check the **priority**: a higher-priority plugin may be returning `blockRequest()` first + +### "Permission denied" inside a hook + +You tried to use a global that requires a permission you didn't request. Add it to `plugin.json`: + +```json +{ + "requires": { "permissions": ["network", "file-read"] } +} +``` + +### "Plugin reloaded but my changes don't show" + +Dev mode has a 500ms debounce. Wait a moment, then check the server log for `PLUGIN_DEV_MODE` events. + +--- + +## What's Next? + +- **[Plugin SDK Reference](./PLUGIN_SDK.md)** — Full API surface, manifest schema, built-in events +- **[Plugin Marketplace](./PLUGIN_MARKETPLACE.md)** — Discover, install, and publish plugins +- **[CLI Plugin Reference](../dev/plugins.md)** — Extend the `omniroute` CLI +- **[Source code](../../src/lib/plugins/)** — Implementation reference (17 files in `src/lib/plugins/`) diff --git a/docs/plugins/PLUGIN_MARKETPLACE.md b/docs/plugins/PLUGIN_MARKETPLACE.md new file mode 100644 index 000000000000..77b0588fc78b --- /dev/null +++ b/docs/plugins/PLUGIN_MARKETPLACE.md @@ -0,0 +1,354 @@ +--- +title: "Plugin Marketplace" +version: 3.8.16 +lastUpdated: 2026-06-08 +--- + +# Plugin Marketplace + +> **TL;DR**: The marketplace is a curated registry of OmniRoute plugins. In **Phase 1** (current), it's a local seed registry. **Phase 2** will introduce a remote registry with ratings, downloads, and signature verification. + +**Source:** `src/lib/plugins/marketplace.ts` + +--- + +## Architecture + +``` +┌────────────────────────────────────────────┐ +│ Marketplace API │ +│ (listMarketplacePlugins, searchMarketplace│ +│ getMarketplaceEntry) │ +└────────────────┬───────────────────────────┘ + │ + ┌─────────┴─────────┐ + ▼ ▼ + Phase 1 Phase 2 (planned) + ─────── ─────────────── + Local seed Remote registry + registry with ratings, + (3 entries) downloads, signed + packages +``` + +### Phase 1: Local Seed Registry (v3.8.16+) + +The local seed registry ships with **4 verified plugins** maintained by the OmniRoute team: + +| Plugin | Tags | Description | +| ---------------- | ----------------------------- | ------------------------------------------------------ | +| `request-logger` | logging, debugging | Logs all requests and responses with timing | +| `rate-limiter` | rate-limit, security | Per-model rate limiting with sliding window | +| `cost-tracker` | analytics, cost | Track token costs per request and per model | +| `theme-manager` | theme, ui, css, customization | Dynamic UI theme management via CSS variable injection | + +All four are `verified: true` and `license: MIT`. + +### Phase 2: Remote Registry (Planned) + +The remote registry will add: + +- **Author submissions** — anyone can publish a plugin +- **Ratings** — 0-5 stars from user reviews +- **Downloads** — install count +- **Signature verification** — Ed25519 signatures ensure authenticity +- **Search by tag, author, license** + +--- + +## API Reference + +All marketplace functions are exported from `omniroute/plugins/marketplace`. + +### `listMarketplacePlugins()` + +List all available plugins in the marketplace. + +```ts +import { listMarketplacePlugins } from "omniroute/plugins/marketplace"; + +const plugins = listMarketplacePlugins(); +for (const p of plugins) { + console.log(`${p.name}@${p.version} — ${p.description} (${p.rating}★)`); +} +``` + +**Returns:** `MarketplaceEntry[]` + +### `searchMarketplace(query)` + +Search marketplace plugins by name, description, or tags. + +```ts +import { searchMarketplace } from "omniroute/plugins/marketplace"; + +const loggingPlugins = searchMarketplace("logging"); +const securityPlugins = searchMarketplace("security"); +``` + +**Parameters:** + +- `query` (string) — case-insensitive search string + +**Returns:** `MarketplaceEntry[]` — matches where `name`, `description`, or any `tag` contains the query + +### `getMarketplaceEntry(name)` + +Get a specific marketplace entry by name. + +```ts +import { getMarketplaceEntry } from "omniroute/plugins/marketplace"; + +const entry = getMarketplaceEntry("request-logger"); +if (entry) { + console.log(`Found: ${entry.name}@${entry.version}`); +} +``` + +**Parameters:** + +- `name` (string) — exact plugin name + +**Returns:** `MarketplaceEntry | undefined` + +### `isMarketplaceAvailable()` + +Check if the marketplace is reachable. + +```ts +import { isMarketplaceAvailable } from "omniroute/plugins/marketplace"; + +if (isMarketplaceAvailable()) { + console.log("Marketplace is online"); +} +``` + +**Returns:** `boolean` — currently always `true` (local seed always available). Will reflect remote registry health in Phase 2. + +--- + +## Data Model + +### `MarketplaceEntry` + +```ts +interface MarketplaceEntry { + name: string; // e.g. "request-logger" + version: string; // e.g. "1.0.0" + description: string; // human-readable summary + author: string; // e.g. "omniroute", "your-name" + license: string; // SPDX license ID, e.g. "MIT" + downloadUrl: string; // URL to download the package + repository?: string; // optional source repo URL + tags: string[]; // search tags + downloads: number; // total install count + rating: number; // 0-5 star average + verified: boolean; // official verification flag + lastUpdated: string; // ISO date of last release +} +``` + +### Field Semantics + +| Field | Required | Notes | +| ------------- | -------- | --------------------------------------------------------------- | +| `name` | ✅ yes | Must be unique, kebab-case, match the plugin's `plugin.json` | +| `version` | ✅ yes | SemVer string | +| `description` | ✅ yes | One-line summary shown in lists | +| `author` | ✅ yes | Display name or org | +| `license` | ✅ yes | SPDX identifier (MIT, Apache-2.0, GPL-3.0, etc.) | +| `downloadUrl` | ✅ yes | URL to the package tarball/zip | +| `repository` | ❌ no | Source code URL (recommended) | +| `tags` | ✅ yes | At least 1 tag for search | +| `downloads` | ✅ yes | Cumulative install count | +| `rating` | ✅ yes | 0.0 to 5.0, one decimal place | +| `verified` | ✅ yes | True if published by OmniRoute team or signed by trusted author | +| `lastUpdated` | ✅ yes | ISO 8601 date string | + +--- + +## Accessing the Marketplace + +The SDK marketplace is accessible **programmatically** via the marketplace API (`src/lib/plugins/marketplace.ts`), not through the CLI. + +### Programmatic Access + +```ts +import { listMarketplacePlugins, searchMarketplace } from "omniroute/plugins/marketplace"; + +// List all seed plugins +const allPlugins = await listMarketplacePlugins(); +console.log(allPlugins); // [request-logger, rate-limiter, cost-tracker, theme-manager] + +// Search by tag +const loggingPlugins = await searchMarketplace("logging"); +console.log(loggingPlugins); // [request-logger] + +// Search by name +const tracker = await searchMarketplace("cost"); +console.log(tracker); // [cost-tracker] +``` + +### Note: CLI Plugin System is Separate + +The CLI command `omniroute plugin search` searches npm for **CLI plugins** (packages named `omniroute-cmd-*`), not the SDK marketplace. These are two separate plugin systems: + +- **SDK plugins** (this doc): Hook-based request/response interception. Managed via the marketplace API. +- **CLI plugins** (separate system): Command-line extensions. Searched and installed via npm. + +--- + +## Discovering Plugins + +### Tag-Based Discovery + +Common tags used in the registry: + +| Tag | What it covers | +| ------------ | -------------------------------------- | +| `logging` | Request/response logging, audit trails | +| `debugging` | Inspection, tracing, dev tools | +| `rate-limit` | Throttling, quota enforcement | +| `security` | Auth, IP filtering, PII masking | +| `analytics` | Usage tracking, cost monitoring | +| `cost` | Token pricing, budget guards | +| `transform` | Request/response rewriting | +| `cache` | Response caching, memoization | +| `provider` | Custom provider integrations | +| `combo` | Combo routing customizations | + +### Search Tips + +- **Multi-word queries**: `search("rate limit")` matches `rate-limit` in tags (hyphen-normalized) +- **Tag-only search**: `search("analytics")` returns plugins tagged `analytics` OR `cost` (loose match) +- **Author search**: not supported in Phase 1; will be added in Phase 2 + +--- + +## Publishing a Plugin (Phase 2) + +When the remote registry goes live, the publishing flow will be: + +### 1. Prepare Your Plugin + +- `plugin.json` manifest with all required fields +- Source code under `lib/` or `src/` +- Compiled entry point (`index.js` or `index.mjs`) +- `README.md` with usage examples +- `LICENSE` file +- `CHANGELOG.md` with version history + +### 2. Generate a Signature (Optional but Recommended) + +See [Plugin Signing & Verification](./PLUGIN_DEVELOPMENT.md#plugin-signing--verification) for the full signing workflow. + +### 3. Submit to the Registry + +> **⚠️ `omniroute plugin publish` does not exist.** `bin/cli/commands/plugin.mjs` exports the subcommands `list`, `install`, `remove` (alias `uninstall`), `info`, `search`, `update`, and `scaffold` — there is no `publish` command (`grep -n "publish" bin/cli/commands/plugin.mjs` returns nothing). The marketplace registry seeds are bundled with OmniRoute and have empty `downloadUrl`; a remote publishing flow is planned but not yet implemented. Plugin authors should distribute plugins through npm (the `omniroute plugin search` CLI queries the npm registry for `omniroute-cmd-*` packages) or via direct `git+https` URLs. + +The current submission path is therefore: + +```bash +# 1. Distribute as an npm package (the only registry wired into the CLI) +npm publish + +# 2. Or, distribute via a git URL and install it locally: +omniroute plugin install git+https://github.com//.git +``` + +### 4. Versioning and Updates + +- Bump the version in `plugin.json` and `package.json` +- Update `CHANGELOG.md` +- Re-publish to npm (or push a new git tag) +- Users will pick up updates via `omniroute plugin update [name]` + +### 5. Ratings and Trust + +Users can rate installed plugins 1-5 stars. The aggregate rating is shown in the marketplace. Plugins with consistently low ratings may be flagged for review. + +--- + +## Verified Plugins + +The `verified: true` flag means the plugin is published by the **OmniRoute team** or by an author whose public key has been added to the trusted set. + +| Plugin | Author | Verified | License | Description | +| ---------------- | --------- | -------- | ------- | ------------------------------------------------------ | +| `request-logger` | omniroute | ✅ | MIT | Logs all requests and responses with timing | +| `rate-limiter` | omniroute | ✅ | MIT | Per-model rate limiting with sliding window | +| `cost-tracker` | omniroute | ✅ | MIT | Track token costs per request and per model | +| `theme-manager` | omniroute | ✅ | MIT | Dynamic UI theme management via CSS variable injection | + +When the remote registry launches, third-party authors can apply for verification by: + +1. Maintaining a public source repository +2. Signing all releases +3. Demonstrating active maintenance +4. Passing a security review + +--- + +## Ratings and Trust + +### Current State (Phase 1) + +- Seed plugins have initial ratings set by the OmniRoute team: `request-logger: 5`, `rate-limiter: 5`, `cost-tracker: 4`, `theme-manager: 5` +- No user ratings yet +- All plugins are `verified: true` by definition + +### Phase 2 Plan + +- 1-5 star ratings from any user who installed the plugin +- Aggregate rating shown as `4.7★ (123 ratings)` style +- Trust tiers: + - **`verified: true`** — published by OmniRoute team OR signature verified + - **`verified: false`** — third-party plugin, not yet reviewed + - **`flagged`** — under security review, not installable + +### Quality Signals + +The marketplace UI will surface: + +- Star rating with count +- Download count +- Last update date +- "Verified" badge +- Author reputation (Phase 2) +- Compatibility with current OmniRoute version + +--- + +## Migration Plan (Phase 1 → Phase 2) + +The current `MarketplaceEntry` shape is designed to be forward-compatible: + +| Field | Phase 1 (now) | Phase 2 (future) | +| ------------- | ----------------------------- | -------------------------------- | +| `downloadUrl` | Empty string for seed entries | Real tarball URLs | +| `downloads` | 0 for seed entries | Cumulative real count | +| `rating` | 5.0 for seed entries | Aggregated user ratings | +| `verified` | Always `true` for seed | Computed from signature + author | +| `lastUpdated` | Release date | Auto-updated on new version | + +**No breaking changes** are planned for the public API. New fields may be added. + +--- + +## Source Code Reference + +| File | Lines | Purpose | +| -------------------------------- | ----- | ------------------------------------- | +| `src/lib/plugins/marketplace.ts` | 107 | Marketplace data + API | +| `src/lib/plugins/manager.ts` | 410+ | Install/activate/deactivate lifecycle | +| `src/lib/plugins/loader.ts` | 280+ | Plugin loading and validation | +| `src/lib/plugins/manifest.ts` | 200+ | Manifest schema validation | +| `src/lib/plugins/signing.ts` | 34 | SHA-256 + Ed25519 verification | + +--- + +## What's Next? + +- **[Plugin SDK Reference](./PLUGIN_SDK.md)** — Full API surface, manifest schema, built-in events +- **[Plugin Development Guide](./PLUGIN_DEVELOPMENT.md)** — Dev mode, testing, doctor, lifecycle +- **[Phase 2 Roadmap](#)** — Remote registry with ratings, downloads, signing (coming soon) diff --git a/docs/plugins/PLUGIN_SDK.md b/docs/plugins/PLUGIN_SDK.md index b0ef67dfe4ae..717d209208c5 100644 --- a/docs/plugins/PLUGIN_SDK.md +++ b/docs/plugins/PLUGIN_SDK.md @@ -1,5 +1,25 @@ # OmniRoute Plugin SDK +> **Related guides:** +> +> - [Plugin Development Guide](./PLUGIN_DEVELOPMENT.md) — dev mode, testing, doctor, signing, lifecycle +> - [Plugin Marketplace](./PLUGIN_MARKETPLACE.md) — discover, install, and publish plugins +> - [CLI Plugin System](../dev/plugins.md) — extend the `omniroute` CLI + +## Two Plugin Systems + +OmniRoute has **two parallel plugin systems** that serve different purposes: + +| System | Where it runs | Purpose | Reference | +| -------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------- | +| **SDK plugins** (this doc) | In-process sandboxed VM inside the OmniRoute server | Hook-based request/response interception (onRequest, onResponse, onError) | Below | +| **CLI plugins** | Separate Node.js process invoked by the `omniroute` binary | Add new subcommands to the CLI (like `gh extension` or `kubectl plugin`) | [CLI Plugin Reference](../dev/plugins.md) | + +You can use either or both. A typical setup might have: + +- An **SDK plugin** that adds rate limiting to incoming requests +- A **CLI plugin** that exposes a `omniroute ratelimit status` command to inspect the rate limiter state + ## Quick Start ```ts @@ -28,12 +48,14 @@ export default definePlugin({ Factory function that creates a Plugin object with defaults. **Parameters:** + - `name` (string, required) — Plugin name in kebab-case - `priority` (number, optional, default: 100) — Lower runs first -- `enabled` (boolean, optional, default: true) — Start enabled? - `onRequest` (function, optional) — Runs before chat handler - `onResponse` (function, optional) — Runs after chat handler - `onError` (function, optional) — Runs on handler error +- `onPluginMessage` (function, optional) — Receives IPC messages from other plugins via `__omniroute.broadcast()` / `__omniroute.sendTo()` +- `onRender` (function, optional) — Renders a dashboard page when a user visits the plugin's admin page ### `blockRequest(response?): BlockingHookResult` @@ -41,7 +63,7 @@ Block the request and optionally return a custom response. ```ts onRequest: (ctx) => { - if (!ctx.headers["authorization"]) { + if (!ctx.apiKeyInfo) { return blockRequest({ error: "Unauthorized", status: 401 }); } }; @@ -69,15 +91,14 @@ onRequest: (ctx) => { ## Plugin Context (`PluginContext`) -| Field | Type | Description | -|---|---|---| -| `requestId` | `string` | Unique request identifier | -| `model` | `string` | Requested model name | -| `provider` | `string` | Target provider ID | -| `body` | `Record` | Request body | -| `headers` | `Record` | Request headers | -| `metadata` | `Record` | Mutable metadata | -| `timestamp` | `number` | Request timestamp | +| Field | Type | Description | +| ------------ | ------------------------- | ------------------------------- | +| `requestId` | `string` | Unique request identifier | +| `model` | `string` | Requested model name | +| `provider` | `string` | Target provider ID | +| `body` | `Record` | Request body | +| `apiKeyInfo` | `unknown` | API key info (if authenticated) | +| `metadata` | `Record` | Mutable metadata | ## Manifest (`plugin.json`) @@ -91,7 +112,9 @@ onRequest: (ctx) => { "hooks": { "onRequest": { "enabled": true, "priority": 50 }, "onResponse": true, - "onError": false + "onError": false, + "onActivate": true, + "onDeactivate": true }, "requires": { "permissions": ["network", "file-read"] @@ -132,17 +155,89 @@ Or as simple booleans (default priority 100): ## Permission System -Plugins run in a sandboxed VM context. Access to external resources requires explicit permissions: +Plugins run in an isolated child process. Access to external resources requires explicit permissions: + +| Permission | Grants | +| ------------ | ------------------------------------------------------------------------------------------ | +| `network` | `fetch`, `AbortController`, `Headers`, `Request`, `Response` | +| `file-read` | `fs.readFile`, `fs.readdir`, `fs.stat` (scoped to plugin's directory) | +| `file-write` | `fs.writeFile`, `fs.mkdir`, `fs.rm` (scoped to plugin's directory) | +| `env` | Read-only `process.env` proxy | +| `exec` | `child_process.exec`, `child_process.execSync` (requires `OMNIROUTE_PLUGINS_ALLOW_EXEC=1`) | +| `db` | `__omniroute.db` — persistent key-value store (SQLite-backed, isolated per plugin) | +| `ipc` | `__omniroute.broadcast()` / `__omniroute.sendTo()` — cross-plugin messaging | + +Without a permission, the corresponding globals are simply not available. + +## IPC Messaging + +Plugins can communicate with each other using the global `__omniroute` API. Requires `"ipc"` permission. + +- `__omniroute.broadcast(event, data)` — Send a message to ALL active plugins +- `__omniroute.sendTo(targetPluginName, event, data)` — Send a message to a specific plugin + +Receiving plugin exports: + +```ts +export function onPluginMessage(payload) { + // payload = { source: "sender-plugin", event: "eventName", data: { ... } } + console.log(`Received "${payload.event}" from ${payload.source}`); +} +``` + +## Database (Key-Value Store) + +Plugins can persist data across restarts using a built-in SQLite-backed key-value store. Requires `"db"` permission. +Each plugin's data is isolated to its own namespace — no two plugins can read each other's data. + +```ts +// Global API available inside plugin sandbox (child process) +__omniroute.db.set("myKey", { any: "JSON-serializable value" }); +const val = __omniroute.db.get("myKey"); // returns parsed value +__omniroute.db.delete("myKey"); +const keys = __omniroute.db.list(); // all keys for this plugin +``` + +## UI Extensibility (Admin Pages) + +Plugins can register custom pages in the OmniRoute dashboard sidebar. Declare them in `plugin.json`: + +```json +{ + "adminPages": [ + { "slug": "dashboard", "title": "My Plugin", "icon": "extension", "position": 10 }, + { "slug": "settings", "title": "Settings", "parent": "my-plugin-dashboard" } + ] +} +``` + +The plugin exports `onRender` to serve page content: + +```ts +export function onRender(payload) { + const { slug, params } = payload; + if (slug === "dashboard") { + return { + type: "html", + html: "

Plugin Dashboard

Welcome!

", + }; + } +} +``` + +Registered pages appear in the sidebar "Plugins" section automatically. The sidebar fetches `/api/plugins/ui-extensions` on mount and renders dynamic menu items. -| Permission | Grants | -|---|---| -| `network` | `fetch`, `AbortController`, `Headers`, `Request`, `Response` | -| `file-read` | `fs.readFile`, `fs.readdir`, `fs.stat` | -| `file-write` | `fs.writeFile`, `fs.mkdir`, `fs.rm` | -| `env` | Read-only `process.env` proxy | -| `exec` | `child_process.exec`, `child_process.execSync` | +## Dashboard Widgets -Without a permission, the corresponding globals are simply not available in the sandbox. +Plugins can declare dashboard widgets in their manifest: + +```json +{ + "dashboardWidgets": [ + { "id": "my-stats", "title": "Usage Stats", "position": "top", "width": "half" } + ] +} +``` ## Config Schema @@ -167,22 +262,24 @@ Config values are persisted in the database and accessible via the dashboard con ## Built-in Events -| Event | When | Payload | -|---|---|---| -| `onRequest` | Before chat handler | Request context | -| `onResponse` | After chat handler | Response data | -| `onError` | On handler error | Error object | -| `onModelSelect` | Model selected for routing | Model info | -| `onComboResolve` | Combo routing resolved | Combo targets | -| `onRateLimit` | Rate limit hit | Limit info | -| `onQuotaExhaust` | Quota exhausted | Quota info | -| `onProviderError` | Provider returned error | Error details | -| `onStreamStart` | SSE stream started | Stream info | -| `onStreamEnd` | SSE stream ended | Stream stats | -| `onInstall` | Plugin installed | `{ name, version, manifest }` | -| `onActivate` | Plugin activated | `{ name, version, manifest }` | -| `onDeactivate` | Plugin deactivated | `{ name, version, manifest }` | -| `onUninstall` | Plugin uninstalled (before files deleted) | `{ name, version, manifest }` | +| Event | When | Payload | +| ----------------- | ----------------------------------------- | ------------------------------------------------------ | +| `onRequest` | Before chat handler | Request context | +| `onResponse` | After chat handler | Response data | +| `onError` | On handler error | Error object | +| `onModelSelect` | Model selected for routing | Model info | +| `onComboResolve` | Combo routing resolved | Combo targets | +| `onRateLimit` | Rate limit hit | Limit info | +| `onQuotaExhaust` | Quota exhausted | Quota info | +| `onProviderError` | Provider returned error | Error details | +| `onStreamStart` | SSE stream started | Stream info | +| `onStreamEnd` | SSE stream ended | Stream stats | +| `onInstall` | Plugin installed | `{ name, version, manifest }` | +| `onActivate` | Plugin activated | `{ name, version, manifest }` | +| `onDeactivate` | Plugin deactivated | `{ name, version, manifest }` | +| `onUninstall` | Plugin uninstalled (before files deleted) | `{ name, version, manifest }` | +| `onPluginMessage` | IPC message from another plugin | `{ source, event, data }` | +| `onRender` | Dashboard page requested | `{ slug, params }` — return HTML or structured content | ## Examples @@ -194,7 +291,7 @@ import { definePlugin } from "omniroute/plugins/sdk"; export default definePlugin({ name: "request-logger", onRequest: async (ctx) => { - console.log(`[${new Date().toISOString()}] ${ctx.method} ${ctx.model} -> ${ctx.provider}`); + console.log(`[${new Date().toISOString()}] ${ctx.model} -> ${ctx.provider || "unknown"}`); }, }); ``` @@ -210,12 +307,12 @@ export default definePlugin({ name: "rate-limiter", priority: 10, onRequest: async (ctx) => { - const key = ctx.headers["x-api-key"] || "anonymous"; + const key = ctx.requestId || "anonymous"; const now = Date.now(); - const window = 60000; // 1 minute + const window = 60000; const maxRequests = 100; - const timestamps = (requests.get(key) || []).filter(t => t > now - window); + const timestamps = (requests.get(key) || []).filter((t) => t > now - window); timestamps.push(now); requests.set(key, timestamps); @@ -226,21 +323,46 @@ export default definePlugin({ }); ``` -### Response Transformer +### Inter-Plugin Communication + +Plugin A broadcasts an event, Plugin B receives it. + +**plugin-a/index.mjs:** ```ts -import { definePlugin } from "omniroute/plugins/sdk"; +export function onRequest(ctx) { + __omniroute.broadcast("request:started", { model: ctx.model, provider: ctx.provider }); +} +``` -export default definePlugin({ - name: "response-transformer", - onResponse: async (ctx, response) => { - if (response.choices) { - response.choices = response.choices.map((c: any) => ({ - ...c, - message: { ...c.message, content: c.message.content.trim() }, - })); - } - return response; - }, -}); +**plugin-b/index.mjs:** + +```ts +export function onPluginMessage(payload) { + console.log(`Plugin B heard: ${payload.event} from ${payload.source}`); + __omniroute.db.set("lastRequest", payload.data); +} +``` + +### Dashboard Admin Page with Widget + +```json +{ + "name": "my-dashboard-plugin", + "hooks": { "onRender": true }, + "adminPages": [{ "slug": "stats", "title": "Stats", "icon": "bar_chart" }], + "dashboardWidgets": [{ "id": "quick-stats", "title": "Quick Stats", "width": "half" }] +} +``` + +```ts +export function onRender(payload) { + if (payload.slug === "stats") { + const count = __omniroute.db.get("requestCount") || 0; + return { + type: "html", + html: `

Request Stats

Total requests tracked: ${count}

`, + }; + } +} ``` diff --git a/docs/reference/openapi.yaml b/docs/reference/openapi.yaml index 1ae4e9ab05f1..06161d5e5a62 100644 --- a/docs/reference/openapi.yaml +++ b/docs/reference/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.8.26 + version: 3.8.27 description: | OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible endpoint that routes requests to multiple AI providers with load balancing, diff --git a/electron/package-lock.json b/electron/package-lock.json index a42c1dbfc888..641e835c4d74 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute-desktop", - "version": "3.8.26", + "version": "3.8.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute-desktop", - "version": "3.8.26", + "version": "3.8.27", "license": "MIT", "dependencies": { "electron-updater": "^6.8.9" diff --git a/electron/package.json b/electron/package.json index 2f07ac06cde5..52919d9ab191 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.8.26", + "version": "3.8.27", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/open-sse/package.json b/open-sse/package.json index 22fc9005deef..3a3ecc763adb 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.8.26", + "version": "3.8.27", "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", "type": "module", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 6be7d680157b..b76d121f6009 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.8.26", + "version": "3.8.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.8.26", + "version": "3.8.27", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -27928,7 +27928,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.8.26" + "version": "3.8.27" } } } diff --git a/package.json b/package.json index ea8323a251d7..e83e69fb65c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.8.26", + "version": "3.8.27", "description": "Unified AI router with 160+ providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", "type": "module", "bin": {