Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- **feat(api):** add `/v1/ocr` endpoint (Mistral OCR), an OCR provider category, and Mistral moderation support. (thanks @waguriagentic)
- **Discovery tool (Phase 2):** add the `discoveryResults` DB module (CRUD over the `discovery_results` table, migration 074) and wire the opt-in provider-discovery service to persist and read findings through it (`persistDiscoveryResult`, `getDiscoveryResults`, `getDiscoveryResultById`, `markVerified`, `deleteDiscoveryResult`) with `(provider, method, endpoint)` upsert de-duplication. Adds the `/api/discovery/*` HTTP surface — `GET /results`, `GET|DELETE /results/:id`, `POST /scan`, `POST /verify/:id` — under **strict loopback-only** authorization (`/api/discovery/` is in `LOCAL_ONLY_API_PREFIXES` and is NOT manage-scope-bypassable, so the `scan` route's outbound probes can never be reached from a tunnel/remote origin). Adds a **dashboard UI tab** (Tools → Discovery, `/dashboard/discovery`) to run scans and review, verify, or delete findings. The service stays **opt-in / default-off**.
- **feat(api):** expose a read-only provider plugin manifest at `GET /api/v1/provider-plugin-manifest` for sidecar/relay discovery. (thanks @KooshaPari)

### 🔧 Bug Fixes

Expand Down
17 changes: 17 additions & 0 deletions docs/reference/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Complete reference for all OmniRoute API endpoints.
- [Embeddings](#embeddings)
- [Image Generation](#image-generation)
- [List Models](#list-models)
- [Provider Plugin Manifest](#provider-plugin-manifest)
- [Compatibility Endpoints](#compatibility-endpoints)
- [Files API](#files-api)
- [Batches API](#batches-api)
Expand Down Expand Up @@ -178,6 +179,22 @@ Selecting this id (e.g. in a Claude Code config that always attaches a `thinking

---

## Provider Plugin Manifest

```bash
GET /api/v1/provider-plugin-manifest
```

Returns the JSON-safe provider plugin manifest used by Bifrost, CLIProxyAPI, and
future sidecar routers. The response is generated from the TypeScript provider
registry and intentionally excludes OAuth client secrets, runtime environment
resolution, executor functions, request headers, and account data.

Use this endpoint when a sidecar runs out-of-process and cannot import
`open-sse/config/providerPluginManifestRegistry.ts` directly.

---

## Compatibility Endpoints

| Method | Path | Format |
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/PROVIDER_PLUGIN_MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ CLIProxyAPI, or a future Go/Rust router. The TypeScript registry remains the
source of truth, but sidecars can consume the manifest without importing
executor code, OAuth defaults, headers, or process environment state.

The same manifest is available over HTTP at
`GET /api/v1/provider-plugin-manifest` for sidecars that run out-of-process.

## Goal

Move provider metadata toward a plugin contract so the hot request path can
Expand Down
24 changes: 24 additions & 0 deletions src/app/api/v1/provider-plugin-manifest/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CORS_HEADERS } from "@/shared/utils/cors";
import { generateProviderPluginManifest } from "@omniroute/open-sse/config/providerPluginManifestRegistry.ts";

const JSON_HEADERS = {
...CORS_HEADERS,
"Content-Type": "application/json",
"Cache-Control": "public, max-age=60",
} as const;

export async function OPTIONS() {
return new Response(null, {
headers: {
...CORS_HEADERS,
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "*",
},
});
}

export async function GET() {
return new Response(JSON.stringify(generateProviderPluginManifest()), {
headers: JSON_HEADERS,
});
}
4 changes: 4 additions & 0 deletions stryker.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"tap": {
"testFiles": [
"tests/unit/account-fallback-anthropic-quota.test.ts",
"tests/unit/account-fallback-retry-after-json.test.ts",
"tests/unit/account-fallback-route-restriction-403.test.ts",
"tests/unit/account-fallback-service.test.ts",
"tests/unit/api-key-rotator-health.test.ts",
Expand Down Expand Up @@ -197,10 +198,13 @@
"tests/unit/chatcore-upstream-timeouts.test.ts",
"tests/unit/circuit-breaker-registry-cap.test.ts",
"tests/unit/circuit-breaker-stream-controller-4602.test.ts",
"tests/unit/clinepass-provider.test.ts",
"tests/unit/codex-session-affinity-reset-aware-5903.test.ts",
"tests/unit/combo-account-allowlist-3266.test.ts",
"tests/unit/combo-headroom-strategy.test.ts",
"tests/unit/combo-model-lockout-honors-reset-1308.test.ts",
"tests/unit/combo-param-validation-fallback-4519.test.ts",
"tests/unit/combo-priority-quota-exhaustion-cutoff-5923.test.ts",
"tests/unit/combo-quota-share-cooldown-wait.test.ts",
"tests/unit/combo-selected-connection-success.test.ts",
"tests/unit/combo-stream-readiness-fallback.test.ts",
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/api/v1/provider-plugin-manifest-route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
GET,
OPTIONS,
} from "../../../../src/app/api/v1/provider-plugin-manifest/route.ts";

test("provider plugin manifest route returns JSON-safe manifest", async () => {
const response = await GET();
const body = await response.json();

assert.equal(response.status, 200);
assert.equal(response.headers.get("Content-Type"), "application/json");
assert.equal(body.schemaVersion, 1);
assert.equal(body.generatedFrom, "open-sse/config/providers");
assert.ok(body.providers.length > 100);
assert.ok(body.providers.some((provider: { id: string }) => provider.id === "openai"));

const serialized = JSON.stringify(body);
assert.equal(serialized.includes("clientSecret"), false);
});

test("provider plugin manifest route handles CORS preflight", async () => {
const response = await OPTIONS();

assert.equal(response.status, 200);
assert.equal(response.headers.get("Access-Control-Allow-Methods"), "GET, OPTIONS");
assert.equal(response.headers.get("Access-Control-Allow-Headers"), "*");
});
Loading