Skip to content
Open
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
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ rec serve
Keep this terminal open. When `flint is ready.` appears, connect a client from
another terminal.

To expose the server on your LAN, bind to all interfaces:

```bash
rec serve --host 0.0.0.0
```

From another machine, connect to `http://<this-mac's-IP>:11500/v1` with the
`rec auth token` key. Traffic is unencrypted, so only use this on trusted
networks.

To use your own install of pi or OpenCode, run:

```bash
Expand Down Expand Up @@ -114,6 +124,76 @@ The Anthropic-compatible API is available at
`http://127.0.0.1:11500` and send the token from `rec auth token` as a bearer
token or as `x-api-key`. The model is `flint`.

<details>
<summary>Connect pi or OpenCode on another machine</summary>

On the Mac running rec, collect three values:

- **Base URL**: `http://<this-mac's-IP>:11500/v1`.
- **API key**: the output of `rec auth token`.
- **Context**: shown by `rec status` (262,144 tokens by default). Use it as both
the context and output limit below. rec fits each reply to the space left.

On the other machine, make the key available to the harness:

```bash
export REC_API_KEY='<key from rec auth token>'
```

**pi**: add the provider to `~/.pi/agent/models.json`. If the file exists, merge
`rec` into its `providers`.

```json
{
"providers": {
"rec": {
"baseUrl": "http://<server-mac's-IP>:11500/v1",
"api": "openai-completions",
"apiKey": "REC_API_KEY",
"models": [{
"id": "flint", "name": "Flint", "reasoning": false, "input": ["text"],
"contextWindow": 262144, "maxTokens": 262144,
"compat": {
"supportsStore": false, "supportsDeveloperRole": false,
"supportsReasoningEffort": false, "supportsStrictMode": false,
"maxTokensField": "max_tokens"
}
}]
}
}
}
```

Select it with `/model`, or start pi with `pi --provider rec --model flint`.

**OpenCode 2.x**: add the provider to `~/.config/opencode/opencode.json`. If the
file exists, merge `rec` into its `providers`.

```json
{
"model": "rec/flint",
"providers": {
"rec": {
"package": "@opencode/ai/providers/openai-compatible",
"name": "Recursive",
"settings": { "baseURL": "http://<server-mac's-IP>:11500/v1", "apiKey": "{env:REC_API_KEY}" },
"models": {
"flint": {
"name": "Flint",
"limit": { "context": 262144, "output": 65536 },
"body": { "max_completion_tokens": 262144 },
"capabilities": { "tools": true, "input": ["text"], "output": ["text"] }
}
}
}
}
}
```

`model` makes Flint the default; remove it to keep yours.

</details>


## Commands

Expand Down
15 changes: 14 additions & 1 deletion cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ func run(args []string) error {
runtimeDir := flags.String("runtime-dir", "", "")
var contextTokens int
port := 11500
var host string
if command == "serve" {
flags.IntVar(&contextTokens, "context", 0, "")
flags.IntVar(&port, "port", port, "")
flags.StringVar(&host, "host", "127.0.0.1", "")
}
flags.SetOutput(io.Discard)
if e := flags.Parse(rest); e != nil {
Expand All @@ -107,6 +109,14 @@ func run(args []string) error {
if port < 1 || port > 65535 {
return fmt.Errorf("port must be between 1 and 65535")
}
if command == "serve" {
// Canonicalize so the engine, Status.Listen and sameInstance agree on one spelling.
addr, err := parseHost(host)
if err != nil {
return err
}
host = addr.String()
}
m, e := loadManifest(*manifest)
if e == nil && command == "serve" {
flags.Visit(func(f *flag.Flag) {
Expand Down Expand Up @@ -139,7 +149,7 @@ func run(args []string) error {
}
return e
}
return serve(ctx, p, m, *runtimeDir, port, os.Stderr, background)
return serve(ctx, p, m, *runtimeDir, host, port, os.Stderr, background)
}

func doctor(p Paths, m Manifest, manifestErr error) error {
Expand Down Expand Up @@ -193,5 +203,8 @@ func printStatus(ui *screen, action string, s Status) {
}
if action == "status" {
ui.println(faintStyle.Render(fmt.Sprintf(" Context %s tokens · rec %s", formatTokens(s.Context), s.Version)))
if listen := networkListen(s); listen != "" {
ui.println(faintStyle.Render(" Listen " + listen + " · reachable from the network"))
}
}
}
58 changes: 46 additions & 12 deletions cli/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,41 +52,75 @@ func TestReleaseManifestContext(t *testing.T) {
func TestExistingInstanceUsesEffectiveProfile(t *testing.T) {
m := Manifest{Model: "flint", Revision: "test", RuntimeSHA: "runtime", Context: 4096}
s := Status{Ready: true, Model: m.Model, Revision: m.Revision, Runtime: m.RuntimeSHA,
Context: m.Context, Instance: instanceKey(m), Endpoint: "http://127.0.0.1:11500/v1"}
if !sameInstance(s, m, 11500) {
Context: m.Context, Instance: instanceKey(m), Listen: "127.0.0.1:11500", Endpoint: "http://127.0.0.1:11500/v1"}
if !sameInstance(s, m, "127.0.0.1", 11500) {
t.Fatal("identical instance rejected")
}
explicit, _ := withContext(m, 4096)
if !sameInstance(s, explicit, 11500) {
if !sameInstance(s, explicit, "127.0.0.1", 11500) {
t.Fatal("explicit default rejected")
}
changed := m
changed.Context = 16384
if sameInstance(s, changed, 11500) {
if sameInstance(s, changed, "127.0.0.1", 11500) {
t.Fatal("reused a smaller context")
}
changed = m
changed.MinRAM++
if sameInstance(s, changed, 11500) {
if sameInstance(s, changed, "127.0.0.1", 11500) {
t.Fatal("reused different serving settings")
}
s.Instance = ""
if sameInstance(s, m, 11500) {
if sameInstance(s, m, "127.0.0.1", 11500) {
t.Fatal("legacy server without profile must restart")
}
}

func TestContextReachesEngine(t *testing.T) {
func TestSameInstanceRequiresSameHost(t *testing.T) {
m := Manifest{Model: "flint", Revision: "test", RuntimeSHA: "runtime", Context: 4096}
wildcard := Status{Ready: true, Model: m.Model, Revision: m.Revision, Runtime: m.RuntimeSHA,
Context: m.Context, Instance: instanceKey(m), Listen: "0.0.0.0:11500", Endpoint: "http://127.0.0.1:11500/v1"}
if sameInstance(wildcard, m, "127.0.0.1", 11500) {
t.Fatal("reused a server bound to a different host")
}
if !sameInstance(wildcard, m, "0.0.0.0", 11500) {
t.Fatal("a server bound to all interfaces was rejected")
}
specific := Status{Ready: true, Model: m.Model, Revision: m.Revision, Runtime: m.RuntimeSHA,
Context: m.Context, Instance: instanceKey(m), Listen: "192.168.1.5:11500", Endpoint: "http://192.168.1.5:11500/v1"}
if sameInstance(specific, m, "127.0.0.1", 11500) {
t.Fatal("reused a server bound to another interface")
}
if !sameInstance(specific, m, "192.168.1.5", 11500) {
t.Fatal("an instance on a specific interface was rejected")
}
legacy := Status{Ready: true, Model: m.Model, Revision: m.Revision, Runtime: m.RuntimeSHA,
Context: m.Context, Instance: instanceKey(m), Endpoint: "http://127.0.0.1:11500/v1"}
if sameInstance(legacy, m, "127.0.0.1", 11500) {
t.Fatal("legacy server without listen address must restart")
}
}

func TestHostAndContextReachesEngine(t *testing.T) {
m := releaseManifest(t)
_, args, _ := launchCommand(Paths{}, m, "/runtime", "/model/flint-int4.json", 11500)
found := false
_, args, _ := launchCommand(Paths{}, m, "/runtime", "/model/flint-int4.json", "127.0.0.1", 11500)
hostArg := ""
for i, arg := range args {
if arg == "--host" && i+1 < len(args) {
hostArg = args[i+1]
}
}
if hostArg != "127.0.0.1" {
t.Fatalf("the requested host did not reach the engine: %q", hostArg)
}
contextLimit := ""
for i, arg := range args {
if arg == "--context-limit" && i+1 < len(args) {
found = args[i+1] == "262144"
contextLimit = args[i+1]
}
}
if !found {
t.Fatal("default 256K allocation did not reach the engine")
if contextLimit != "262144" {
t.Fatalf("default 256K allocation did not reach the engine: %q", contextLimit)
}
if _, err := withContext(m, 524288); err == nil {
t.Fatal("512K exceeds the engine limit")
Expand Down
4 changes: 2 additions & 2 deletions cli/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
"strconv"
)

func launchCommand(p Paths, m Manifest, runtimeDir, model string, port int) (string, []string, []string) {
func launchCommand(p Paths, m Manifest, runtimeDir, model, host string, port int) (string, []string, []string) {
return filepath.Join(runtimeDir, runtimeServer), []string{
"serve", m.Model, "--model-dir", filepath.Dir(model), "--manifest", model,
"--port", strconv.Itoa(port), "--context-limit", strconv.Itoa(m.Context),
"--host", host, "--port", strconv.Itoa(port), "--context-limit", strconv.Itoa(m.Context),
"--api-key-file", filepath.Join(p.Config, "api-key"),
}, []string{"HF_HUB_OFFLINE=1", "TOKENIZERS_PARALLELISM=false"}
}
Expand Down
3 changes: 2 additions & 1 deletion cli/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ var commands = []commandHelp{
{"exec", "pi|opencode ARGS", "Run a harness command, such as installing a plugin", nil},
{"serve", "[MODEL]", "Start the model server in this terminal", append([][2]string{
{"--context TOKENS", "Total input and output tokens (default: set by the release)"},
{"--port PORT", "Local port (default: 11500)"},
{"--port PORT", "Port (default: 11500)"},
{"--host ADDRESS", "IP address to listen on (default: 127.0.0.1; 0.0.0.0 for all interfaces)"},
}, developmentOptions...)},
{"status", "", "Show whether the server is running", nil},
{"stop", "", "Stop the server", nil},
Expand Down
6 changes: 5 additions & 1 deletion cli/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/netip"
"net/url"
"os"
"os/exec"
Expand Down Expand Up @@ -309,7 +310,10 @@ func serverPhase(log string) string {

func validateClientStatus(s Status) error {
endpoint, err := url.Parse(s.Endpoint)
if err != nil || endpoint.Scheme != "http" || endpoint.Hostname() != "127.0.0.1" || endpoint.Port() == "" || endpoint.Path != "/v1" || endpoint.User != nil || endpoint.RawQuery != "" || endpoint.Fragment != "" {
if err != nil || endpoint.Scheme != "http" || endpoint.Port() == "" || endpoint.Path != "/v1" || endpoint.User != nil || endpoint.RawQuery != "" || endpoint.Fragment != "" {
return fmt.Errorf("invalid local Rec endpoint")
}
if _, err := netip.ParseAddr(endpoint.Hostname()); err != nil {
return fmt.Errorf("invalid local Rec endpoint")
}
if !s.Ready || s.Model != "flint" || s.Context < 2048 {
Expand Down
23 changes: 23 additions & 0 deletions cli/launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,26 @@ func TestHarnessShellCommand(t *testing.T) {
t.Fatalf("shell command does not reproduce the environment: %v %q", err, out)
}
}

func TestValidateClientStatusEndpoint(t *testing.T) {
for _, tc := range []struct {
name string
endpoint string
ok bool
}{
{"loopback", "http://127.0.0.1:11500/v1", true},
{"lan", "http://192.168.1.5:11500/v1", true},
{"ipv6 loopback", "http://[::1]:11500/v1", true},
{"https", "https://127.0.0.1:11500/v1", false},
{"hostname", "http://localhost:11500/v1", false},
{"missing port", "http://127.0.0.1/v1", false},
} {
t.Run(tc.name, func(t *testing.T) {
s := Status{Ready: true, Model: "flint", Context: 8192, Endpoint: tc.endpoint}
err := validateClientStatus(s)
if (err == nil) != tc.ok {
t.Fatalf("validateClientStatus(%q) error = %v, want ok = %v", tc.endpoint, err, tc.ok)
}
})
}
}
2 changes: 2 additions & 0 deletions cli/pi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ func TestClientsRejectInvalidStatus(t *testing.T) {
{Ready: true, Model: "other", Context: 8192, Endpoint: "http://127.0.0.1:11500/v1"},
{Ready: false, Model: "flint", Context: 8192, Endpoint: "http://127.0.0.1:11500/v1"},
{Ready: true, Model: "flint", Context: 1024, Endpoint: "http://127.0.0.1:11500/v1"},
{Ready: true, Model: "flint", Context: 8192, Endpoint: "http://localhost:11500/v1"},
{Ready: true, Model: "flint", Context: 8192, Endpoint: "http://127.0.0.1/v1"},
} {
p := Paths{Data: t.TempDir()}
if _, _, err := piConfig(p, s); err == nil {
Expand Down
Loading