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
20 changes: 19 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
# Database
DATABASE_URL=postgresql://legal_ai:changeme@localhost:5432/legal_ai
OPENAI_API_KEY=
DB_PASSWORD=changeme

# LLM provider (agent only — MCP/CLI/API don't use the LLM)
# Or use: npx @robotixai/lexius-agent --provider openai|openrouter|ollama
LEXIUS_MODEL_PROVIDER=anthropic
LEXIUS_MODEL=

# Anthropic (default provider)
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-sonnet-4-6
ANTHROPIC_MODEL_REASONING=claude-opus-4-6
ANTHROPIC_MODEL_STRUCTURED=claude-sonnet-4-6

# OpenAI (also used for embeddings)
OPENAI_API_KEY=

# OpenRouter (single key for any model — Claude, GPT-4, Llama, Gemini, etc.)
OPENROUTER_API_KEY=

# Ollama (local, no key needed)
OLLAMA_URL=http://localhost:11434/v1

# API server
PORT=3000
NODE_ENV=development
LOG_LEVEL=info
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to this project are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/).
Versioning follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- **Model harness** — provider-agnostic LLM abstraction. The agent no longer imports `@anthropic-ai/sdk` directly; it uses a `CompletionProvider` interface with 5 implementations:
- `AnthropicProvider` (default)
- `OpenAIProvider` (GPT-4o, o1, o3)
- `OpenRouterProvider` (single key for any model — Claude, GPT-4, Llama, Gemini)
- `OllamaProvider` (local models, no API key)
- `MockProvider` (canned responses for testing)
- `--provider` CLI flag: `npx @robotixai/lexius-agent --provider openrouter`
- Specflow contract `model_harness.yml` (2 rules: no SDK imports in agent code, providers don't import domain)
- **Offshore CIMA** — 10 Cayman Islands acts ingested via PDF source adapter. 650 sections, 1,200 extracts (88 KYD fines, 28 imprisonment terms, 1,082 shall-clauses).
- Source adapter interface (`CellarAdapter` + `PdfAdapter`)
- Common-law section parser with title/body merge + dynamic header detection
- CIMA registry (10 acts with verified PDF URLs)
- `fine_amount_kyd` + `imprisonment_term` extract types (migration 0005)
- Specflow contract `offshore_adapters.yml` (4 rules)

## [0.3.0] - 2026-04-17

### Added
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ npx @robotixai/lexius-cli audit --legislation eu-ai-act --description "recruitme

## Contract Enforcement

19 contracts, 43 rules enforced by [Specflow](https://www.npmjs.com/package/@robotixai/specflow-cli):
20 contracts, 45 rules enforced by [Specflow](https://www.npmjs.com/package/@robotixai/specflow-cli):

```bash
npx @robotixai/specflow-cli enforce .
Expand All @@ -348,6 +348,7 @@ npx @robotixai/specflow-cli enforce .
| **Integration** | `integration_security` | No key hashes in responses; SSE uses auth |
| **Swarm** | `hivemind_swarm` | No LLM in agent loop; atomic claims; cleanup complete |
| **Offshore** | `offshore_adapters` | No LLM in PDF parsing; source_format=pdf; section merge; dynamic header detection |
| **Model Harness** | `model_harness` | No direct SDK imports in agent code; providers don't import domain |
| **Fetcher** | `fetcher_verbatim` | Records sourceHash + fetchedAt |
| **Audit** | `audit_report_integrity`, `audit_enhancement_layer`, `audit_agent_layer` | GenerateAuditReport is deterministic; enhancement via port |
| **Security** | `security_secrets`, `security_sql_safety`, `security_input_validation`, `security_no_eval` | No hardcoded creds; parameterised queries; Zod validation |
Expand All @@ -361,7 +362,7 @@ pnpm --filter @lexius/core test # 183 unit tests
pnpm --filter @lexius/api test # 36 functional tests
pnpm --filter @lexius/fetcher test # 78 extractor + parser tests
pnpm crosscheck # Penalty cross-check vs verbatim law
npx @robotixai/specflow-cli enforce . # 19 contracts, 43 rules
npx @robotixai/specflow-cli enforce . # 20 contracts, 45 rules
```

## Documentation
Expand Down Expand Up @@ -399,7 +400,7 @@ Full spec documents in `docs/`:
- **Bundler:** esbuild
- **Monorepo:** Turborepo + pnpm workspaces
- **PDF Parsing:** pdfjs-dist (offshore legislation)
- **Contracts:** Specflow (19 contracts, 43 rules)
- **Contracts:** Specflow (20 contracts, 45 rules)
- **Testing:** Vitest + Supertest (297 tests)

## License
Expand Down
19 changes: 19 additions & 0 deletions docs/ard/ARD-015-model-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ Ollama exposes an OpenAI-compatible `/v1/chat/completions` endpoint. `OllamaProv

No separate SDK dependency. Same `openai` package, different config.

### 4a. OpenRouter reuses OpenAI provider with unified model access

OpenRouter (`openrouter.ai`) provides a single OpenAI-compatible API that routes to hundreds of models across providers (Anthropic, OpenAI, Meta, Google, Mistral, etc.). One API key, one endpoint, unified billing.

`OpenRouterProvider` extends `OpenAIProvider` with:
- `baseURL: "https://openrouter.ai/api/v1"`
- `apiKey: process.env.OPENROUTER_API_KEY`
- `defaultModel: "anthropic/claude-sonnet-4"` (or whatever the user configures via `LEXIUS_MODEL`)

Same pattern as Ollama — a thin subclass, no new SDK dependency. The `openai` package works directly against OpenRouter's endpoint.

Why this matters:
- **Single key for everything** — users don't need separate Anthropic, OpenAI, and Google keys. One OpenRouter key accesses all of them.
- **Model comparison** — run the same compliance query against `anthropic/claude-sonnet-4`, `openai/gpt-4o`, and `google/gemini-2.0-flash` to compare tool-use quality. The harness handles it transparently.
- **Cost routing** — OpenRouter supports model fallbacks and cost-optimised routing. A user can configure `auto` as the model and let OpenRouter pick the cheapest model that handles tool-use.
- **No vendor relationship needed** — useful for users who can't or won't sign up directly with each provider.

Rejected: building a custom multi-provider router. OpenRouter already solves this at the API level; wrapping it is ~5 lines of code.

### 5. Provider selection is a factory function, not dependency injection

```typescript
Expand Down
1 change: 0 additions & 1 deletion docs/ard/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,3 @@
- [ARD-014: Hivemind Swarm](ARD-014-hivemind-swarm.md) — Postgres-backed stigmergic swarm, Promise.all concurrency, deterministic agents, gap detection
- [ARD-015: Model Harness](ARD-015-model-harness.md) — single CompletionProvider interface, provider-internal translation, factory selection via env var
- [ARD-016: Offshore CIMA](ARD-016-offshore-cima.md) — PDF source adapter, common-law section parser, CIMA registry, pdfjs-dist
- [ARD-015: Model Harness](ARD-015-model-harness.md) — single CompletionProvider interface, provider-internal translation, factory selection via env var
66 changes: 57 additions & 9 deletions docs/ddd/DDD-014-model-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,20 @@

## Overview

Implementation details for PRD-011 / ARD-015. Covers: normalised types, three provider implementations, factory, agent refactor, mock for testing, and the Specflow contract.
Implementation details for PRD-011 / ARD-015. Covers: normalised types, five provider implementations (Anthropic, OpenAI, OpenRouter, Ollama, Mock), factory, agent refactor, and the Specflow contract.

## Package Structure

```
packages/agent/src/providers/
├── types.ts -- ChatParams, ChatResponse, ContentBlock, ToolDefinition
├── anthropic.ts -- AnthropicProvider (native SDK)
├── openai.ts -- OpenAIProvider (OpenAI SDK)
├── openrouter.ts -- OpenRouterProvider (extends OpenAI, routes to any model)
├── ollama.ts -- OllamaProvider (extends OpenAI, local models)
├── mock.ts -- MockProvider (canned responses for tests)
└── index.ts -- createProvider factory + getDefaultModel
```

## Normalised Types

Expand Down Expand Up @@ -242,6 +255,35 @@ export class OllamaProvider extends OpenAIProvider {

Three lines. Ollama's API is OpenAI-compatible.

## OpenRouterProvider

```typescript
// packages/agent/src/providers/openrouter.ts
import { OpenAIProvider } from "./openai.js";

export class OpenRouterProvider extends OpenAIProvider {
constructor() {
super({
apiKey: process.env.OPENROUTER_API_KEY,
baseURL: "https://openrouter.ai/api/v1",
});
}
}
```

Same pattern as Ollama — a thin subclass. OpenRouter's API is fully OpenAI-compatible with tool-use support. Users select models via the standard model parameter using OpenRouter's naming convention:

```bash
npx @robotixai/lexius-agent --provider openrouter
# defaults to anthropic/claude-sonnet-4

LEXIUS_MODEL=openai/gpt-4o npx @robotixai/lexius-agent --provider openrouter
LEXIUS_MODEL=meta-llama/llama-3-70b npx @robotixai/lexius-agent --provider openrouter
LEXIUS_MODEL=google/gemini-2.0-flash npx @robotixai/lexius-agent --provider openrouter
```

One API key, any model. No need for separate provider accounts.

## MockProvider

```typescript
Expand Down Expand Up @@ -289,6 +331,10 @@ export function createProvider(override?: string): CompletionProvider {
const { OpenAIProvider } = require("./openai.js");
return new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY });
}
case "openrouter": {
const { OpenRouterProvider } = require("./openrouter.js");
return new OpenRouterProvider();
}
case "ollama": {
const { OllamaProvider } = require("./ollama.js");
return new OllamaProvider();
Expand All @@ -299,18 +345,19 @@ export function createProvider(override?: string): CompletionProvider {
}
default:
throw new Error(
`Unknown model provider: ${provider}. Valid: anthropic, openai, ollama, mock`,
`Unknown model provider: ${provider}. Valid: anthropic, openai, openrouter, ollama, mock`,
);
}
}

export function getDefaultModel(provider?: string): string {
switch (provider || process.env.LEXIUS_MODEL_PROVIDER || "anthropic") {
case "anthropic": return process.env.LEXIUS_MODEL || "claude-sonnet-4-6";
case "openai": return process.env.LEXIUS_MODEL || "gpt-4o";
case "ollama": return process.env.LEXIUS_MODEL || "llama3";
case "mock": return "mock";
default: return "claude-sonnet-4-6";
case "anthropic": return process.env.LEXIUS_MODEL || "claude-sonnet-4-6";
case "openai": return process.env.LEXIUS_MODEL || "gpt-4o";
case "openrouter": return process.env.LEXIUS_MODEL || "anthropic/claude-sonnet-4";
case "ollama": return process.env.LEXIUS_MODEL || "llama3";
case "mock": return "mock";
default: return "claude-sonnet-4-6";
}
}
```
Expand Down Expand Up @@ -364,10 +411,11 @@ The recursive tool-use loop stays identical. Only the types change from `Anthrop

| Variable | Default | Description |
|----------|---------|-------------|
| `LEXIUS_MODEL_PROVIDER` | `anthropic` | Provider: `anthropic`, `openai`, `ollama`, `mock` |
| `LEXIUS_MODEL` | per-provider | Model override |
| `LEXIUS_MODEL_PROVIDER` | `anthropic` | Provider: `anthropic`, `openai`, `openrouter`, `ollama`, `mock` |
| `LEXIUS_MODEL` | per-provider | Model override (e.g., `anthropic/claude-sonnet-4` for OpenRouter) |
| `ANTHROPIC_API_KEY` | -- | Required for `anthropic` provider |
| `OPENAI_API_KEY` | -- | Required for `openai` provider |
| `OPENROUTER_API_KEY` | -- | Required for `openrouter` provider. Single key for all models. |
| `OLLAMA_URL` | `http://localhost:11434/v1` | Ollama API URL |

## Testing Strategy
Expand Down
1 change: 0 additions & 1 deletion docs/ddd/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@
- [DDD-013: Hivemind Swarm](DDD-013-hivemind-swarm.md) — compliance_workspace + swarm_work_queue tables, agent loop, gap detector, synthesis, API/MCP integration
- [DDD-014: Model Harness](DDD-014-model-harness.md) — AnthropicProvider, OpenAIProvider, OllamaProvider, MockProvider, factory, agent refactor
- [DDD-015: Offshore CIMA](DDD-015-offshore-cima.md) — PdfAdapter, section parser, CIMA registry, dollar/imprisonment extractors, ingest refactor
- [DDD-014: Model Harness](DDD-014-model-harness.md) — AnthropicProvider, OpenAIProvider, OllamaProvider, MockProvider, factory, agent refactor
7 changes: 5 additions & 2 deletions docs/prd/PRD-011-model-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@ Cost-based routing is an optional layer on top: a router examines the user's que

6. **`OllamaProvider`** — wraps Ollama's OpenAI-compatible API (`http://localhost:11434/v1/chat/completions`). Uses the same translation as `OpenAIProvider` but with Ollama-specific defaults (no API key, local URL). Supports any model available in Ollama (llama3, mistral, qwen, etc.).

7. **`MockProvider`** — returns canned responses for testing. Configurable: can return a fixed text response, a fixed tool_use response, or echo the input. Used in unit tests so they don't hit any API.
7. **`OpenRouterProvider`** — wraps OpenRouter's OpenAI-compatible API (`https://openrouter.ai/api/v1`). Uses the same translation as `OpenAIProvider` but with OpenRouter-specific defaults. Provides access to hundreds of models (Claude, GPT-4, Llama, Mistral, Gemini, etc.) through a single API key and unified billing. The key advantage: users can switch between any model from any provider without managing multiple API keys. Supports model routing via the standard model parameter (e.g., `anthropic/claude-sonnet-4`, `openai/gpt-4o`, `meta-llama/llama-3-70b`).

8. **Provider selection via environment** — `LEXIUS_MODEL_PROVIDER` env var selects the provider:
8. **`MockProvider`** — returns canned responses for testing. Configurable: can return a fixed text response, a fixed tool_use response, or echo the input. Used in unit tests so they don't hit any API.

9. **Provider selection via environment** — `LEXIUS_MODEL_PROVIDER` env var or `--provider` CLI flag selects the provider:
- `anthropic` (default) — uses `ANTHROPIC_API_KEY`
- `openai` — uses `OPENAI_API_KEY` (the same one used for embeddings)
- `openrouter` — uses `OPENROUTER_API_KEY`. Access any model via a single key.
- `ollama` — uses `OLLAMA_URL` (default `http://localhost:11434`)
- `mock` — no API key needed

Expand Down
19 changes: 13 additions & 6 deletions packages/agent/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @robotixai/lexius-agent

Interactive AI compliance consultant for the [Lexius](https://github.com/rob-otix-ai/lexius) platform. Powered by Claude with deterministic tool use — every factual claim comes from the database, not the model's training data.
Interactive AI compliance consultant for the [Lexius](https://github.com/rob-otix-ai/lexius) platform. Provider-agnostic — works with Anthropic, OpenAI, OpenRouter, or Ollama. Every factual claim comes from the database, not the model's training data.

## Quick Start

Expand All @@ -12,10 +12,15 @@ docker run -d -p 5432:5432 \
-e POSTGRES_USER=$POSTGRES_USER \
robotixai/lexius-db

# 2. Run the agent
# 2. Run the agent (default: Anthropic)
export DATABASE_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB
export ANTHROPIC_API_KEY=sk-ant-...
npx @robotixai/lexius-agent

# Or use a different provider
npx @robotixai/lexius-agent --provider openai # requires OPENAI_API_KEY
npx @robotixai/lexius-agent --provider openrouter # requires OPENROUTER_API_KEY (any model, one key)
npx @robotixai/lexius-agent --provider ollama # local models, no key needed
```

## What It Does
Expand Down Expand Up @@ -46,7 +51,7 @@ The agent is configured for maximum reproducibility:
| `classify_system` | Risk classification | DB (deterministic) |
| `get_obligations` | Obligations by role/risk | DB |
| `calculate_penalty` | Penalty calculation | DB + extracted values |
| `get_article` | Verbatim article text | CELLAR (AUTHORITATIVE) |
| `get_article` | Verbatim article text | CELLAR/PDF (AUTHORITATIVE) |
| `get_deadlines` | Compliance deadlines | DB |
| `search_knowledge` | Semantic search | DB + embeddings |
| `answer_question` | FAQ lookup | DB |
Expand All @@ -72,9 +77,11 @@ await cleanupSession(db, result.sessionId);
| Variable | Required | Description |
|----------|----------|-------------|
| `DATABASE_URL` | Yes | PostgreSQL connection string |
| `ANTHROPIC_API_KEY` | Yes | Anthropic API key |
| `ANTHROPIC_MODEL` | No | Model override (default: `claude-sonnet-4-6`) |
| `OPENAI_API_KEY` | No | For embeddings in semantic search |
| `ANTHROPIC_API_KEY` | --provider anthropic | Anthropic API key (default provider) |
| `OPENAI_API_KEY` | --provider openai | OpenAI API key (also used for embeddings) |
| `OPENROUTER_API_KEY` | --provider openrouter | Single key for any model (Claude, GPT-4, Llama, Gemini) |
| `OLLAMA_URL` | --provider ollama | Ollama API URL (default: localhost:11434) |
| `LEXIUS_MODEL` | No | Override the default model for any provider |

## Links

Expand Down
7 changes: 4 additions & 3 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"@lexius/core": "workspace:*",
"@lexius/db": "workspace:*",
"@lexius/infra": "workspace:*",
"@lexius/logger": "workspace:*",
"drizzle-orm": "^0.35.0",
"@lexius/logger": "workspace:*"
"openai": "^4.70.0",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0"
},
"devDependencies": {
"typescript": "^5.8.3"
Expand Down
Loading
Loading