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
2 changes: 1 addition & 1 deletion .devrail.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ languages:
# - terraform
# - ansible
# - ruby
# - go
- go
# - javascript
# - rust

Expand Down
166 changes: 79 additions & 87 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,122 +1,114 @@
# Project Name
# DevRail Router

> Built with [DevRail](https://devrail.dev) `v1` standards. See [STABILITY.md](STABILITY.md) for component status.
> Local-first LLM routing and control plane for private AI infrastructure.

<!-- TODO: Replace with your project name and one-line description -->

A new project bootstrapped from the [DevRail GitHub template](https://github.com/devrail-dev/github-repo-template).

<!-- badges-start -->
<!-- TODO: Add CI status badge: ![Lint](https://github.com/OWNER/REPO/actions/workflows/lint.yml/badge.svg) -->
[![DevRail compliant](https://devrail.dev/images/badge.svg)](https://devrail.dev)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
<!-- badges-end -->

## Quick Start

1. Click **"Use this template"** on [github.com/devrail-dev/github-repo-template](https://github.com/devrail-dev/github-repo-template) to create a new repository.
2. Edit `.devrail.yml` and uncomment the languages used in your project.
3. Run `make install-hooks` to set up pre-commit hooks.

## Usage
DevRail Router presents one OpenAI-compatible endpoint to local agents and
developer tools, then routes requests to private inference backends such as LM
Studio, Ollama, vLLM, SGLang, or approved cloud fallbacks.

The Makefile is the universal execution interface. Every target produces consistent behavior whether invoked by a developer, CI pipeline, or AI agent.
The initial target user is an operator running mixed self-hosted inference
hardware who wants a private subscription-style backend for tools such as
Hermes, OpenClaw, opencode, and other local agents.

| Target | Purpose |
|---|---|
| `make help` | Show available targets (default) |
| `make lint` | Run all linters for declared languages |
| `make format` | Run all formatters for declared languages |
| `make fix` | Auto-fix formatting issues in-place |
| `make test` | Run project test suite |
| `make security` | Run language-specific security scanners |
| `make scan` | Run universal scanning (trivy, gitleaks) |
| `make docs` | Generate documentation |
| `make check` | Run all of the above; report composite summary |
| `make install-hooks` | Install pre-commit and pre-push hooks |
## Status

All targets except `help` and `install-hooks` delegate to the dev-toolchain Docker container (`ghcr.io/devrail-dev/dev-toolchain:v1`).
This repository is in early foundation work. The current service supports:

## Configuration
- a small Go HTTP service
- `/healthz`
- `/v1/models`
- OpenAI-compatible `/v1/*` request proxying
- model alias rewriting
- YAML configuration
- Linux/systemd packaging notes

### `.devrail.yml`
Routing policy, auth, telemetry, LM Studio lifecycle integration, and Omarchy
integration are planned next.

Every DevRail-managed repository includes a `.devrail.yml` file at the repo root. This file declares the project's languages and settings, and is read by the Makefile, CI pipelines, and AI agents.
## Quick Start

```yaml
languages:
- python
- bash
Build and test locally:

fail_fast: false
log_format: json
```sh
go test ./...
go build ./cmd/devrail-router
```

Uncomment the languages used in your project and configure settings as needed.

### Branch Protection

To enforce CI checks before merging pull requests:

1. Go to **Settings > Branches > Branch protection rules**
2. Add a rule for the `main` branch
3. Enable **"Require status checks to pass before merging"**
4. Select all five status checks: `lint`, `format`, `security`, `test`, `docs`
Run against the example config:

### GitHub Template Repository

This repo is configured as a GitHub template. To enable this on your fork:

1. Go to **Settings > General**
2. Check **"Template repository"** under the repository name section
3. Users will then see a **"Use this template"** button on the repo page

## Contributing

See [DEVELOPMENT.md](DEVELOPMENT.md) for development standards, coding conventions, and contribution guidelines.
```sh
go run ./cmd/devrail-router serve -config configs/router.example.yaml
```

To add a new language ecosystem to DevRail, see the [Contributing to DevRail](https://github.com/devrail-dev/devrail-standards/blob/main/standards/contributing.md) guide.
List exposed model aliases:

This project follows [Conventional Commits](https://www.conventionalcommits.org/). All commits use the `type(scope): description` format.
```sh
curl http://127.0.0.1:8080/v1/models
```

## Retrofit Existing Project
Send a chat completion through the router:

To add DevRail standards to an existing GitHub repository:
```sh
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "local-coder",
"messages": [{"role": "user", "content": "Reply with ok."}],
"max_tokens": 32
}'
```

### Step 1: Core Configuration
## Configuration

- [ ] Copy `.devrail.yml` and uncomment your project's languages
- [ ] Copy `.editorconfig`
- [ ] Merge `.gitignore` patterns into your existing .gitignore
- [ ] Copy `Makefile` (or merge targets if you have an existing Makefile)
See `configs/router.example.yaml`.

### Step 2: Pre-Commit Hooks
```yaml
server:
address: 127.0.0.1:8080

models:
- id: local-coder
name: Local Coder
backend: lmstudio
target_model: qwen3-coder-30b-a3b-instruct
context_window: 65536
max_output_tokens: 4096
tool_calls: true

backends:
- id: lmstudio
type: openai-compatible
base_url: http://127.0.0.1:1234/v1
```

- [ ] Copy `.pre-commit-config.yaml` and uncomment hooks for your languages
- [ ] Run `make install-hooks`
## Packaging Direction

### Step 3: Agent Instruction Files
Linux is the first-class target:

- [ ] Copy `DEVELOPMENT.md`, `CLAUDE.md`, `AGENTS.md`, `.cursorrules`
- [ ] Copy `.opencode/agents.yaml`
- Binary: `/usr/local/bin/devrail-router`
- Config: `/etc/devrail/router.yaml`
- State: `/var/lib/devrail-router`
- Service user: `devrail-router`
- Service manager: systemd

### Step 4: CI Workflows
See `docs/packaging.md` and `packaging/systemd/devrail-router.service`.

- [ ] Copy `.github/workflows/` directory (lint.yml, format.yml, security.yml, test.yml, docs.yml)
- [ ] Configure branch protection: Settings > Branches > Require status checks
Omarchy support is planned as a separate integration profile. See
`integrations/omarchy/README.md`.

### Step 5: Project Documentation
## Development

- [ ] Copy `.github/PULL_REQUEST_TEMPLATE.md`
- [ ] Copy `.github/CODEOWNERS` and configure for your team
- [ ] Copy `CHANGELOG.md` if not already present
This project follows [DevRail](https://devrail.dev) development standards.

### Step 6: Verify
```sh
make check
```

- [ ] Run `make check` and fix any issues
- [ ] Create a test commit to verify pre-commit hooks fire
- [ ] Create a test PR to verify CI workflows run
All DevRail checks run through `ghcr.io/devrail-dev/dev-toolchain:v1`.

## License

This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
MIT. See [LICENSE](LICENSE).
126 changes: 126 additions & 0 deletions cmd/devrail-router/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package main

import (
"context"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/devrail-dev/devrail-router/internal/config"
"github.com/devrail-dev/devrail-router/internal/server"
)

const version = "0.1.0-dev"

func main() {
os.Exit(run(os.Args[1:]))
}

func run(args []string) int {
if len(args) == 0 {
args = []string{"serve"}
}

switch args[0] {
case "serve":
return serve(args[1:])
case "check":
return check(args[1:])
case "version":
fmt.Println(version)
return 0
case "help", "-h", "--help":
usage()
return 0
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", args[0])
usage()
return 2
}
}

func serve(args []string) int {
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
configPath := fs.String("config", config.DefaultPath, "path to router config")
if err := fs.Parse(args); err != nil {
return 2
}

cfg, err := config.Load(*configPath)
if err != nil {
slog.Error("load config", "error", err)
return 1
}

handler, err := server.New(cfg)
if err != nil {
slog.Error("create server", "error", err)
return 1
}

httpServer := &http.Server{
Addr: cfg.Server.Address,
Handler: handler,
ReadHeaderTimeout: 15 * time.Second,
}

errCh := make(chan error, 1)
go func() {
slog.Info("starting devrail router", "address", cfg.Server.Address)
errCh <- httpServer.ListenAndServe()
}()

signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)

select {
case err := <-errCh:
if err != nil && err != http.ErrServerClosed {
slog.Error("server stopped", "error", err)
return 1
}
case sig := <-signalCh:
slog.Info("shutting down", "signal", sig.String())
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := httpServer.Shutdown(ctx); err != nil {
slog.Error("shutdown failed", "error", err)
return 1
}
}

return 0
}

func check(args []string) int {
fs := flag.NewFlagSet("check", flag.ContinueOnError)
configPath := fs.String("config", config.DefaultPath, "path to router config")
if err := fs.Parse(args); err != nil {
return 2
}

cfg, err := config.Load(*configPath)
if err != nil {
slog.Error("config invalid", "error", err)
return 1
}

slog.Info("config ok", "address", cfg.Server.Address, "models", len(cfg.Models), "backends", len(cfg.Backends))
return 0
}

func usage() {
fmt.Fprintf(os.Stderr, `DevRail Router %s

Usage:
devrail-router serve [-config path]
devrail-router check [-config path]
devrail-router version

`, version)
}
23 changes: 23 additions & 0 deletions configs/router.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
server:
address: 127.0.0.1:8080

models:
- id: local-coder
name: Local Coder
backend: lmstudio
target_model: qwen3-coder-30b-a3b-instruct
context_window: 65536
max_output_tokens: 4096
tool_calls: true
- id: local-coder-large
name: Local Coder Large
backend: lmstudio
target_model: qwen/qwen3.6-35b-a3b
context_window: 131072
max_output_tokens: 4096
tool_calls: true

backends:
- id: lmstudio
type: openai-compatible
base_url: http://127.0.0.1:1234/v1
Loading
Loading