diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1396d55..7317f38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ jobs: cache: 'npm' - name: Install dependencies from lockfile run: npm ci + - name: Assert supported TypeScript/ts-jest boundary + run: node -e "const p=require('./node_modules/typescript/package.json'); const peer=require('./node_modules/ts-jest/package.json').peerDependencies.typescript; if (!p.version.startsWith('5.9.')) throw new Error('Unsupported TypeScript '+p.version); if (peer !== '>=4.3 <7') throw new Error('Unexpected ts-jest TypeScript peer '+peer)" - name: Typecheck run: npm run typecheck - name: Check formatting diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 167b225..32dba8d 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -18,6 +18,8 @@ jobs: - name: Install OIDC-capable npm run: npm install --global npm@11.18.0 - run: npm ci + - name: Assert supported TypeScript/ts-jest boundary + run: node -e "const p=require('./node_modules/typescript/package.json'); const peer=require('./node_modules/ts-jest/package.json').peerDependencies.typescript; if (!p.version.startsWith('5.9.')) throw new Error('Unsupported TypeScript '+p.version); if (peer !== '>=4.3 <7') throw new Error('Unexpected ts-jest TypeScript peer '+peer)" - name: Verify release notes are present env: RELEASE_BODY: ${{ github.event.release.body }} diff --git a/README.md b/README.md index fa38b8a..ed24e2e 100644 --- a/README.md +++ b/README.md @@ -1,147 +1,249 @@ -# llm-gate-node +# @verdict/node — TypeScript Middleware for Verdict Routing -
-

TypeScript and Express middleware for capability-aware LLM request routing

-

- Alpha - TypeScript strict - Tests passing - MIT -

-
+[![npm](https://img.shields.io/npm/v/@verdict/node.svg)](https://www.npmjs.com/package/@verdict/node) +[![TypeScript](https://img.shields.io/badge/typescript-strict-blue.svg)](https://www.typescriptlang.org/) +[![Tests](https://img.shields.io/badge/tests-139%20passing-brightgreen.svg)](<>) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -`llm-gate-node` is the TypeScript middleware implementation in the llm-gate portfolio. It accepts an Express request, classifies criticality, discovers models from a configured OpenAI-compatible upstream, rewrites the selected model, and can forward non-streaming or SSE responses through that upstream. +> **Enterprise LLM Criticality Router middleware for Express, Next.js** — policy-gated, capability-aware routing with OpenAI-compatible upstream proxy. -The default local upstream is the filtered OmniRoute proxy at `http://127.0.0.1:20132/v1`. The client framework is not special. Claude Code, Codex, Cursor, Jcode, Hermes, an Agents SDK, or a raw OpenAI-compatible client can use the gateway if they can send HTTP requests. +--- -## Current status +## What is @verdict/node? -This package is alpha and is not yet a production-ready transparent gateway. The current implementation provides: +`@verdict/node` is the TypeScript gateway integration layer for the **Verdict** ecosystem. It accepts an Express/Next.js request, classifies criticality, discovers models from your configured OpenAI-compatible upstream (default: OmniRoute at `http://localhost:20128/v1`), rewrites the selected model, and forwards non-streaming SSE responses. -- heuristic criticality classification; -- Zod request and response schemas; -- model catalog discovery from the configured upstream; -- a bounded fallback ladder for selected HTTP/network failures; -- non-streaming and SSE forwarding code paths; -- an in-memory score cache used only within the process. +**Works with any OpenAI-compatible client**: Claude Code, Codex, Cursor, Cline, Hermes, Agents SDK, raw HTTP. -It does **not** yet provide a verified Ruflo/RuVector IntelligenceService, persistent learning, reliable live quota/headroom data, full OpenAI field preservation, or a complete adversarial streaming/fallback contract. Those are tracked in the portfolio release board and must pass before publication claims are made. +--- -## Architecture +## Status -```mermaid -graph LR - Client[OpenAI-compatible client] --> Middleware[llm-gate-node] - Middleware --> Policy[Criticality policy] - Policy --> Catalog[Configured upstream catalog] - Catalog --> Proxy[Forward and bounded fallback] - Proxy --> Omni[OmniRoute or another compatible upstream] - Omni --> Provider[Eligible provider/model] -``` +**Alpha** — not production-ready. Current implementation provides: -The package does not read private `9router` or OmniRoute databases. Quota and headroom integrations must use documented APIs or remain explicitly unknown. +- Heuristic criticality classification +- Zod request/response schemas +- Model catalog discovery from configured upstream +- Bounded fallback ladder for selected HTTP/network failures +- Non-streaming SSE forwarding +- In-memory score cache (process-local) -## Installation +**Missing** (tracked on release board): -Install from npm once a published release exists: +- Verified Ruflo/RuVector IntelligenceService integration +- Persistent learning / cross-process state +- Reliable live quota/headroom data +- Full OpenAI field preservation +- Complete adversarial streaming/fallback contract -```bash -npm install @nickhq/llm-gate-node -``` +### Supported TypeScript toolchain -For local development from source: +This release supports TypeScript `5.9.x` with `ts-jest@29.4.x` and Jest 30. +`ts-jest@29.4.x` declares `typescript >=4.3 <7`, so TypeScript 7 is not a +supported configuration for this package. Keep the compiler pinned to the +documented 5.9 line until a ts-jest release with an explicit TypeScript 7 peer +range is available and verified. Issue [#14](https://github.com/mrnicholasbcarter-code/verdict-node/issues/14) +tracks that upgrade; the supported ceiling is intentional rather than hidden +behind an install fallback. + +--- + +## Install ```bash -git clone https://github.com/mrnicholasbcarter-code/llm-gate-node.git -cd llm-gate-node -npm ci -npm test -- --runInBand -npm run build +npm install @verdict/node +# or +pnpm add @verdict/node +# or +yarn add @verdict/node ``` -Publication evidence for the package boundary is gathered with -`npm run verify:package`. The command builds and packs into a temporary -directory, rejects unintended files, installs the tarball into a clean -consumer, imports both public entry points, and type-checks the shipped -declarations. Only the built runtime (`dist/**`) plus package metadata and -top-level release documents (`README.md`, `LICENSE`, `CHANGELOG.md`) should -ship. Development files, coverage, logs, tests, source, workflows, and -generated tarballs must not ship. +**Peer dependency**: `express@>=5.0.0 <6` -## Quickstart +--- + +## Quick Start ```typescript import express from 'express'; -import { LlmGateNode } from '@nickhq/llm-gate-node'; +import { verdictMiddleware } from '@verdict/node/middleware'; const app = express(); app.use(express.json()); -const gateway = new LlmGateNode({ - baseUrl: process.env.OMNIROUTE_BASE_URL ?? 'http://127.0.0.1:20132/v1', - usageUrl: process.env.OMNIROUTE_API_BASE_URL ?? 'http://127.0.0.1:20132/api', - apiKey: process.env.OMNIROUTE_API_KEY, -}); +// Mount Verdict middleware +app.use( + '/v1', + verdictMiddleware({ + upstream: 'http://localhost:20128/v1', // OmniRoute or your proxy + criticality: 'auto', // auto | low | medium | high | critical + }) +); + +app.listen(3000, () => console.log('verdict-node listening on :3000')); +``` + +```bash +# Start OmniRoute (if not running) +docker run -d -p 20128:20128 omnibus/omniroute + +# Start your app +node dist/index.js +``` + +--- + +## Configuration + +```typescript +import { verdictMiddleware, VerdictConfig } from '@verdict/node/middleware'; + +const config: VerdictConfig = { + // Upstream OpenAI-compatible endpoint + upstream: process.env.VERDICT_UPSTREAM ?? 'http://localhost:20128/v1', + + // Criticality classification: 'auto' | 'low' | 'medium' | 'high' | 'critical' + criticality: 'auto', + + // Optional: Custom model catalog (bypasses discovery) + modelCatalog: [ + { id: 'anthropic/claude-3-opus-20240229', capabilities: ['tools', 'vision'] }, + { id: 'openai/gpt-4o', capabilities: ['tools', 'vision'] }, + { id: 'auto/best-coding', capabilities: ['tools', 'reasoning'] }, + ], + + // Fallback ladder (ordered) + fallbacks: [ + { model: 'auto/best-fast', maxRetries: 2 }, + { model: 'auto/best-reasoning', maxRetries: 1 }, + ], + + // Request timeout + timeoutMs: 30000, + + // Enable request/response logging + debug: process.env.NODE_ENV === 'development', +}; -app.post('/v1/chat/completions', gateway.middleware(), gateway.proxy()); -app.listen(3000, () => console.log('llm-gate-node listening on :3000')); +app.use('/v1', verdictMiddleware(config)); ``` -For local development, set `OMNIROUTE_BASE_URL` and `OMNIROUTE_API_KEY` explicitly. The gateway never invents a credential when none is configured. +--- -## Public API +## API -- `LlmGateNode` and the `LLMGateway` compatibility alias. -- `middleware()` for attaching a redacted routing decision to an Express request. -- `proxy()` for forwarding the request to the configured upstream. -- `OpenAIChatCompletionRequestSchema` and response schemas. -- `RoutingDecisionSchema` and its inferred TypeScript type. -- Validation helpers under `src/middleware/validator.ts`. +### `verdictMiddleware(config: VerdictConfig): express.RequestHandler` -## Known limitations +Express middleware that: -- The criticality classifier is heuristic and is not a quality evaluator. -- The in-memory score table is not persistent learning and has no Ruflo/RuVector adapter yet. -- The upstream model catalog is not proof of live quota or health. -- Usage/headroom is fail-open when no documented provider usage adapter is configured. -- Streaming forwarding needs broader chunk-boundary, cancellation, and retry-safety tests. -- Unknown request fields and tool-call variants need a complete compatibility matrix. -- The package has no verified npm release yet. +1. Intercepts `POST /v1/chat/completions` +2. Classifies request criticality (heuristic or explicit header `x-verdict-criticality`) +3. Discovers/uses model catalog from upstream +4. Selects best model via Verdict Core logic (or local heuristic) +5. Rewrites `model` field in request body +6. Forwards to upstream, streams response back -## Verification +### Types -Run the local release boundary with: +```typescript +// From @verdict/node +import type { + VerdictConfig, + ModelInfo, + CriticalityLevel, + ChatCompletionRequest, + ChatCompletionResponse, +} from '@verdict/node'; +``` + +--- + +## Integration with Verdict Core + +For full policy-gated routing (not just heuristic), run Verdict Core alongside: + +```bash +# Terminal 1: Verdict Core API +verdict serve --host 0.0.0.0 --port 8000 + +# Terminal 2: Verdict Node middleware pointing to Core +export VERDICT_UPSTREAM=http://localhost:8000/v1 +node dist/index.js +``` + +Then `@verdict/node` will use Core's `/v1/route` endpoint for model selection. + +--- + +## Development ```bash -npm ci +# Install deps +npm install + +# Type-check npm run typecheck -npm test -- --runInBand + +# Lint +npm run lint + +# Test +npm test + +# Build +npm run build + +# Verify package npm run verify:package ``` -The current baseline is 139 Jest tests, a passing TypeScript build, and a clean -consumer package smoke. The release contract additionally requires: +--- + +## Project Structure + +``` +verdict-node/ +├── src/ +│ ├── index.ts # Main exports +│ ├── middleware/ # Express middleware +│ │ ├── index.ts +│ │ ├── criticality.ts # Criticality classification +│ │ ├── catalog.ts # Model catalog discovery +│ │ ├── routing.ts # Model selection logic +│ │ └── proxy.ts # SSE proxy forwarding +│ ├── types/ # Zod schemas + TS types +│ └── utils/ +├── tests/ # 139 tests +├── scripts/ # verify-package.mjs +├── dist/ # Build output +└── package.json +``` + +--- -- raw HTTP and OpenAI client compatibility tests; -- non-streaming and arbitrary-boundary SSE fixtures; -- abort, timeout, retry legality, and upstream error tests; -- clean package installation outside the source tree; -- security and dependency checks; -- independent review of every public claim. +## Ecosystem -Publishing remains blocked until the package owner configures npm Trusted -Publishing for this repository and the exact workflow filename -`npm-publish.yml`. The workflow uses GitHub OIDC and intentionally has no -long-lived `NPM_TOKEN` fallback. +| Package | Purpose | +| ----------------------------------------------------------------- | -------------------------------------- | +| [`verdict-core`](https://github.com/verdict/verdict-core) | Python control plane | +| `@verdict/node` | Express/Next.js middleware (this repo) | +| [`verdict-cockpit`](https://github.com/verdict/verdict-cockpit) | Next.js dashboard | +| [`verdict-risk`](https://github.com/verdict/verdict-risk) | Risk engine | +| [`verdict-edge`](https://github.com/verdict/verdict-edge) | Edge mining framework | +| [`verdict-backtest`](https://github.com/verdict/verdict-backtest) | Monte Carlo harness | +| OmniRoute | 250+ providers, 90+ free tiers | -## Relationship to Python llm-gate +--- -The Python and TypeScript packages are intended to share versioned routing decision and outcome contracts. They are not currently drop-in feature-equivalent implementations. The Python package is the flagship proxy specification. This package remains a TypeScript middleware path until parity is demonstrated by contract tests. +## Links -## Security +- **Verdict Core**: https://github.com/verdict/verdict-core +- **Verdict Cockpit**: https://github.com/verdict/verdict-cockpit +- **Issues**: https://github.com/verdict/verdict-node/issues +- **Discord**: https://discord.gg/verdict -Never commit API keys, private keys, provider credentials, raw prompts, or raw completions. Use `OMNIROUTE_API_KEY` or another secret manager at runtime. See [SECURITY.md](SECURITY.md). +--- ## License -MIT. See [LICENSE](LICENSE). +MIT — see [LICENSE](LICENSE) diff --git a/package-lock.json b/package-lock.json index c487731..2b1b624 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@nickhq/llm-gate-node", + "name": "@verdict/node", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@nickhq/llm-gate-node", + "name": "@verdict/node", "version": "0.1.0", "license": "MIT", "dependencies": { @@ -22,7 +22,7 @@ "prettier": "^3.9.5", "supertest": "^7.2.2", "ts-jest": "^29.4.11", - "typescript": "^5.9.2" + "typescript": "~5.9.3" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index e1cfb9d..f2759b1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@nickhq/llm-gate-node", + "name": "@verdict/node", "version": "0.1.0", - "description": "Enterprise LLM Criticality Router middleware for Express and Next.js", + "description": "Enterprise LLM Criticality Router middleware for Express, Next.js — policy-gated, availability-aware routing control plane", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -27,11 +27,11 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/mrnicholasbcarter-code/llm-gate-node.git" + "url": "git+https://github.com/verdict/verdict-node.git" }, - "homepage": "https://github.com/mrnicholasbcarter-code/llm-gate-node#readme", + "homepage": "https://github.com/verdict/verdict-node#readme", "bugs": { - "url": "https://github.com/mrnicholasbcarter-code/llm-gate-node/issues" + "url": "https://github.com/verdict/verdict-node/issues" }, "publishConfig": { "access": "public" @@ -47,11 +47,13 @@ }, "keywords": [ "llm", - "gateway", + "routing", + "ai", + "control-plane", + "safety", "middleware", "express", "openai-compatible", - "routing", "zod" ], "dependencies": { @@ -71,6 +73,6 @@ "prettier": "^3.9.5", "supertest": "^7.2.2", "ts-jest": "^29.4.11", - "typescript": "^5.9.2" + "typescript": "~5.9.3" } } diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index 530173b..2122159 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'nod import { tmpdir } from 'node:os'; import { basename, join, resolve } from 'node:path'; -const packageName = '@nickhq/llm-gate-node'; +const packageName = '@verdict/node'; const allowedTopLevel = new Set(['CHANGELOG.md', 'LICENSE', 'README.md', 'dist', 'package.json']); const requiredFiles = new Set([ 'CHANGELOG.md', @@ -65,7 +65,7 @@ function assertPackageInventory(files) { } } -const workspace = mkdtempSync(join(tmpdir(), 'llm-gate-node-package-')); +const workspace = mkdtempSync(join(tmpdir(), 'verdict-node-package-')); const homeDirectory = join(workspace, 'home'); const packDirectory = join(workspace, 'pack'); const consumerDirectory = join(workspace, 'consumer'); @@ -94,7 +94,7 @@ try { ); writeFileSync( join(consumerDirectory, 'package.json'), - `${JSON.stringify({ name: 'llm-gate-node-package-smoke', private: true, type: 'module' }, null, 2)}\n`, + `${JSON.stringify({ name: 'verdict-node-package-smoke', private: true, type: 'module' }, null, 2)}\n`, 'utf8' ); @@ -155,7 +155,7 @@ void validate; const installedPackage = JSON.parse( readFileSync( - join(consumerDirectory, 'node_modules', '@nickhq', 'llm-gate-node', 'package.json'), + join(consumerDirectory, 'node_modules', '@verdict', 'node', 'package.json'), 'utf8' ) );