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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ models:

You can swap in any model available on OpenRouter — not just Anthropic ones.

## Pareto routing

OpenRouter's [Pareto router](https://openrouter.ai/openrouter/pareto-code) picks the cheapest model that meets a minimum coding quality threshold (`min_coding_score`, 0.0–1.0). Use a postfix on the model ID to set the threshold:

```yaml
models:
opus: "openrouter/pareto-code:high" # score 0.9 — best coding models
sonnet: "openrouter/pareto-code:medium" # score 0.7 — balanced
haiku: "openrouter/pareto-code:low" # score 0.5 — budget
subagent: "openrouter/pareto-code:0.85" # exact numeric value
```

| Postfix | `min_coding_score` |
|---|---|
| `:high` | 0.9 |
| `:medium` / `:mid` | 0.7 |
| `:low` | 0.5 |
| `:0.0`–`:1.0` | as specified |
| `:nitro` | none (OpenRouter speed variant — unchanged) |
| _(none)_ | none (OpenRouter dashboard default) |

The postfix is stripped from the model ID before the request reaches OpenRouter.

## How it works

`orcc claude` replaces itself (via `exec`) with the `claude` process after setting:
Expand Down
74 changes: 74 additions & 0 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -105,6 +106,14 @@ func (p *Proxy) handleMessages(w http.ResponseWriter, r *http.Request) {
log.Printf("handleMessages: injected openrouter:web_search")
}

if scoreBody, scoreChanged, err := extractParetoScore(modified); err != nil {
log.Printf("extractParetoScore: %v", err)
} else if scoreChanged {
modified = scoreBody
changed = true
log.Printf("handleMessages: injected min_coding_score for Pareto model")
}

msgURL := p.target + "/messages"
if r.URL.RawQuery != "" {
msgURL += "?" + r.URL.RawQuery
Expand Down Expand Up @@ -668,6 +677,71 @@ func (p *Proxy) ensureModels() []map[string]string {
return list
}

// ── Pareto router score injection ─────────────────────────────────────────────

// paretoScoreSuffixes maps named tier aliases to min_coding_score values.
var paretoScoreSuffixes = map[string]float64{
"low": 0.5,
"mid": 0.7,
"medium": 0.7,
"high": 0.9,
}

// extractParetoScore inspects the request body's "model" field. If the model
// starts with "openrouter/pareto" and ends with a recognized score suffix
// (numeric 0.0–1.0 or a named alias), the suffix is stripped from the model
// field and min_coding_score is injected via the pareto-router plugin:
//
// "plugins": [{"id": "pareto-router", "min_coding_score": 0.8}]
//
// The ":nitro" suffix is left intact and does not trigger score injection.
// Other suffixes are left untouched.
func extractParetoScore(body []byte) ([]byte, bool, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(body, &raw); err != nil {
return body, false, nil
}

model := jsonStr(raw["model"])
if !strings.HasPrefix(model, "openrouter/pareto") {
return body, false, nil
}

lastColon := strings.LastIndex(model, ":")
if lastColon < 0 {
return body, false, nil
}

base := model[:lastColon]
suffix := model[lastColon+1:]

var score float64
if s, ok := paretoScoreSuffixes[suffix]; ok {
score = s
} else if f, err := strconv.ParseFloat(suffix, 64); err == nil && f >= 0.0 && f <= 1.0 {
score = f
} else {
// Unknown suffix (including :nitro) — pass through unchanged.
return body, false, nil
}

raw["model"] = mustMarshal(base)

plugin := map[string]interface{}{
"id": "pareto-router",
"min_coding_score": score,
}
var plugins []json.RawMessage
if pluginsRaw, ok := raw["plugins"]; ok {
json.Unmarshal(pluginsRaw, &plugins)
}
plugins = append(plugins, mustMarshal(plugin))
raw["plugins"] = mustMarshal(plugins)

out, _ := json.Marshal(raw)
return out, true, nil
}

func mustMarshal(v any) []byte {
data, err := json.Marshal(v)
if err != nil {
Expand Down
130 changes: 130 additions & 0 deletions proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,136 @@ func TestInjectWebSearch(t *testing.T) {
}
}

func TestExtractParetoScore(t *testing.T) {
tests := []struct {
name string
input string
wantModel string
wantScore float64
wantSet bool
}{
{
name: "numeric score",
input: `{"model":"openrouter/pareto-code:0.8","messages":[]}`,
wantModel: "openrouter/pareto-code",
wantScore: 0.8,
wantSet: true,
},
{
name: "high alias",
input: `{"model":"openrouter/pareto-code:high","messages":[]}`,
wantModel: "openrouter/pareto-code",
wantScore: 0.9,
wantSet: true,
},
{
name: "low alias",
input: `{"model":"openrouter/pareto-code:low","messages":[]}`,
wantModel: "openrouter/pareto-code",
wantScore: 0.5,
wantSet: true,
},
{
name: "mid alias",
input: `{"model":"openrouter/pareto-code:mid","messages":[]}`,
wantModel: "openrouter/pareto-code",
wantScore: 0.7,
wantSet: true,
},
{
name: "medium alias",
input: `{"model":"openrouter/pareto-code:medium","messages":[]}`,
wantModel: "openrouter/pareto-code",
wantScore: 0.7,
wantSet: true,
},
{
name: "no suffix",
input: `{"model":"openrouter/pareto-code","messages":[]}`,
wantSet: false,
},
{
name: "nitro suffix untouched",
input: `{"model":"openrouter/pareto-code:nitro","messages":[]}`,
wantSet: false,
},
{
name: "non-pareto model untouched",
input: `{"model":"anthropic/claude-sonnet-4.6","messages":[]}`,
wantSet: false,
},
{
name: "unknown suffix untouched",
input: `{"model":"openrouter/pareto-code:wat","messages":[]}`,
wantSet: false,
},
{
name: "out-of-range numeric untouched",
input: `{"model":"openrouter/pareto-code:1.5","messages":[]}`,
wantSet: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
out, changed, err := extractParetoScore([]byte(tc.input))
if err != nil {
t.Fatalf("extractParetoScore: %v", err)
}

if !tc.wantSet {
if changed {
t.Errorf("expected unchanged, got changed")
}
return
}

if !changed {
t.Fatalf("expected changed, got unchanged")
}

var got map[string]json.RawMessage
if err := json.Unmarshal(out, &got); err != nil {
t.Fatalf("parse output: %v", err)
}

gotModel := jsonStr(got["model"])
if gotModel != tc.wantModel {
t.Errorf("model: got %q, want %q", gotModel, tc.wantModel)
}

var plugins []map[string]json.RawMessage
if err := json.Unmarshal(got["plugins"], &plugins); err != nil || len(plugins) == 0 {
t.Fatalf("plugins missing or unparseable")
}
p := plugins[len(plugins)-1]
if id := jsonStr(p["id"]); id != "pareto-router" {
t.Errorf("plugin id: got %q, want %q", id, "pareto-router")
}
var gotScore float64
if err := json.Unmarshal(p["min_coding_score"], &gotScore); err != nil {
t.Fatalf("parse min_coding_score from plugin: %v", err)
}
if gotScore != tc.wantScore {
t.Errorf("min_coding_score: got %v, want %v", gotScore, tc.wantScore)
}
})
}
}

func TestExtractParetoScoreInvalidJSON(t *testing.T) {
out, changed, err := extractParetoScore([]byte(`{invalid`))
if err != nil {
t.Fatalf("expected no error on invalid JSON, got: %v", err)
}
if changed {
t.Errorf("expected changed=false on invalid JSON")
}
if string(out) != "{invalid" {
t.Errorf("expected original body returned, got: %s", string(out))
}
}

func TestInjectWebSearchInvalidJSON(t *testing.T) {
// Invalid JSON returns original body unchanged (no error)
out, changed, err := injectWebSearch([]byte(`{invalid`))
Expand Down