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
22 changes: 22 additions & 0 deletions cmd/router/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,8 @@ func main() {
// below can be overridden per deployment.
plannerEnabled := config.GetOr("ROUTER_PLANNER_ENABLED", "true") == "true"
scoreToolResultTurns := config.GetOr("ROUTER_SCORE_TOOL_RESULT_TURNS", "true") == "true"
// Keep ToolResult candidates within the requested tier for safer evaluation.
toolResultTierCeiling := config.GetOr("ROUTER_TOOL_RESULT_TIER_CEILING", "false") == "true"
// Defensive backstop for Anthropic's cyber safety classifier: re-pin a
// session off a model that returned a safety refusal. Default off; enabled
// on the defensive deploy. Fallback target when the pin has no runner-up.
Expand Down Expand Up @@ -771,6 +773,7 @@ func main() {
WithSubAgentOverride(subAgentProvider, subAgentModel).
WithPlannerEnabled(plannerEnabled).
WithScoreToolResultTurns(scoreToolResultTurns).
WithToolResultTierCeiling(toolResultTierCeiling).
WithCyberRefusalRepin(cyberRefusalRepin).
WithCyberRefusalFallbackModel(cyberRefusalFallbackModel).
WithPrefixTrimFreeSwitch(prefixTrimFreeSwitch).
Expand Down Expand Up @@ -846,6 +849,18 @@ func main() {
logger.Info("Provider exclusion override active", "excluded_providers", cleaned)
}

if respectRaw := strings.TrimSpace(config.GetOr("ROUTER_RESPECT_REQUESTED_MODEL", "")); respectRaw != "" {
parts := strings.Split(respectRaw, ",")
cleaned := make([]string, 0, len(parts))
for _, p := range parts {
if trimmed := strings.TrimSpace(p); trimmed != "" {
cleaned = append(cleaned, trimmed)
}
}
proxySvc = proxySvc.WithRespectRequestedModel(cleaned)
logger.Info("Requested-model passthrough active", "honored_models", cleaned)
}

// The usage observer is always wired (cheap, side-effect-free) even though
// the cost discount below is env-gated: it also feeds the per-installation
// usage-bypass gate, which is DB-gated and can't know the env flag's state.
Expand Down Expand Up @@ -1070,6 +1085,13 @@ func buildClusterScorer(availableProviders map[string]struct{}) (router.Router,
logger.Info("Cluster top_p overridden", "top_p", n)
}
}
if v := strings.TrimSpace(config.GetOr("ROUTER_STATIC_CLUSTER_PIN", "")); v != "" {
pins := parseStaticClusterPins(v, logger)
if len(pins) > 0 {
cfg.StaticClusterPin = pins
logger.Info("Static cluster pin active", "pins", pins)
}
}
scorers := make(map[string]*cluster.Scorer, len(versions))
warmed := make(map[string]cluster.Embedder)
var defaultEmbedderID string
Expand Down
38 changes: 38 additions & 0 deletions cmd/router/static_cluster_pin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"fmt"
"log/slog"
"strconv"
"strings"

"workweave/router/internal/router/catalog"
)

func parseStaticClusterPins(raw string, logger *slog.Logger) map[int]string {
pins := make(map[int]string)
for _, pair := range strings.Split(raw, ",") {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
clusterID, model, found := strings.Cut(pair, ":")
if !found || strings.TrimSpace(model) == "" {
panic(fmt.Sprintf("invalid ROUTER_STATIC_CLUSTER_PIN entry %q: expected cluster_id:model", pair))
}
id, err := strconv.Atoi(strings.TrimSpace(clusterID))
if err != nil {
panic(fmt.Sprintf("invalid ROUTER_STATIC_CLUSTER_PIN cluster ID in %q", pair))
}
model = strings.TrimSpace(model)
if _, known := catalog.ByID(model); !known {
logger.Warn("Static cluster pin model is not in catalog; skipping",
"cluster_id", id,
"model", model,
)
continue
}
pins[id] = model
}
return pins
}
26 changes: 26 additions & 0 deletions cmd/router/static_cluster_pin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"io"
"log/slog"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseStaticClusterPinsSkipsUnknownModels(t *testing.T) {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))

got := parseStaticClusterPins("0:claude-haiku-4-5,1:not-in-catalog", logger)

require.Equal(t, map[int]string{0: "claude-haiku-4-5"}, got)
}

func TestParseStaticClusterPinsPanicsOnMalformedEntry(t *testing.T) {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))

assert.Panics(t, func() {
parseStaticClusterPins("not-a-pair", logger)
})
}
3 changes: 3 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ Set `DATABASE_URL` directly, or compose it from the individual vars:
| `ROUTER_HARD_PIN_EXPLORE` | `true` | Pin Claude Code Task-tool sub-agent turns to `ROUTER_HARD_PIN_MODEL`/`ROUTER_HARD_PIN_PROVIDER` (or the cheapest deployed model, if those are unset). Set `false` to route sub-agents through the scorer like any other turn. |
| `ROUTER_SUBAGENT_MODEL` | *(none)* | Route Claude Code Task-tool sub-agent turns to a distinct model, independent of `ROUTER_HARD_PIN_MODEL` — e.g. a local/self-hosted OpenAI-compatible model (point `OPENROUTER_BASE_URL` at your local server) while the main loop keeps using Anthropic/whatever the scorer picks. Requires `ROUTER_SUBAGENT_PROVIDER`; either alone is ignored. Takes effect regardless of `ROUTER_HARD_PIN_EXPLORE`, but the HMM strategy keeps its own sub-agent handling and isn't affected. |
| `ROUTER_SUBAGENT_PROVIDER` | *(none)* | Pair with `ROUTER_SUBAGENT_MODEL`. |
| `ROUTER_RESPECT_REQUESTED_MODEL` | *(none)* | Comma-separated models served verbatim when requested, bypassing planner and scorer. Automatic hard pins still win; an explicit force-model override also wins over this list. Excluded or ineligible honored models fall back to automatic routing. |
| `ROUTER_STATIC_CLUSTER_PIN` | *(none)* | `clusterID:model` pairs, comma-separated. A nearest-cluster match skips `blendScoresV2`; an ineligible pinned model falls through to normal routing. Malformed pairs or cluster IDs abort startup; valid pins naming models absent from the catalog are warned and skipped. `Reason` is tagged `cluster-pin:`. |
| `ROUTER_TOOL_RESULT_TIER_CEILING` | `false` | On `ToolResult` turns, excludes models above the requested model's tier on Messages, OpenAI Chat Completions, and Gemini surfaces. It is a no-op when no at-or-below-tier model survives, and a session-pin fit-check cannot lift its exclusions. Unknown requested-model tiers are a no-op. |
| `ROUTER_TRANSLATION_COMPATIBILITY_MODE` | `shadow` | Translation representability rollout: `off` disables broad filtering, `shadow` records candidate exclusions without changing routes, and `enforce` makes declared semantic requirements hard routing constraints. Native-only safety paths (such as unsupported Responses tool unions and native Gemini ingress) remain protected unless mode is `off`. |
| `ROUTER_COMPACTION_PCT` | `0.85` | Fraction of the largest eligible model's context window at which the proactive compaction cascade engages (clear old tool results → structured summary → trim). Range `(0,1]`; `0` disables compaction (over-window requests then 413). Mirrors Claude Code's ~0.85 auto-compact trigger. |
| `ROUTER_ONNX_ASSETS_DIR` | `/opt/router/assets` | Directory containing `model.onnx` + `tokenizer.json`. |
Expand Down
7 changes: 6 additions & 1 deletion internal/proxy/force_model_tier_fallback_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
type tierProbeRouter struct {
available map[string]struct{}
captured []router.Request
provider string
}

func (r *tierProbeRouter) Route(_ context.Context, req router.Request) (router.Decision, error) {
Expand All @@ -41,7 +42,11 @@ func (r *tierProbeRouter) Route(_ context.Context, req router.Request) (router.D
if best == "" {
return router.Decision{}, errors.New("no eligible candidate")
}
return router.Decision{Provider: providers.ProviderAnthropic, Model: best, Reason: "fake"}, nil
provider := r.provider
if provider == "" {
provider = providers.ProviderAnthropic
}
return router.Decision{Provider: provider, Model: best, Reason: "fake"}, nil
}

// forcedPinStore returns a single user-forced pin for every lookup.
Expand Down
Loading
Loading