relay

Profiles & routing

Profiles map task kinds + skills to ordered provider chains. The orchestrator picks the best-matching profile at session start and uses its chain instead of the global fallback.

Anatomy

[profiles.backend]
chain        = ["claude", "codex", "ollama"]
kinds        = ["api", "service", "refactor", "test"]
skills       = ["go", "ts", "rust"]
context_hint = "production backend services"

Matching

score("backend", "add refund flow to orders service") =
  contains("api")        → +3
  contains("service")    → +3
  contains("refactor")   → 0
  contains("test")       → 0
  contains("go")         → 0
  contains("backend")    → 0   (hint word, but not in goal)
  contains("services")   → +1  (hint word)
  total: 7

Best score wins. Tie-break by alphabetical order. Score 0 → no match, falls back to the global provider_priority order.

Outcome boost

After enough sessions, the matcher factors in historical success rate from .relay/outcomes.jsonl:

boosted = baseScore × (0.5 + successRate)

So a profile that historically lands 80% of its tasks gets × 1.3; one at 20% gets × 0.7. Brand-new profiles default to 0.5 success (neutral).

Editing in the app

Profiles page in relay-ui:

Writes back to .relay/relay.toml immediately; daemon reloads.

Editing the TOML

[profiles.frontend]
chain        = ["claude", "codex"]
kinds        = ["component", "style", "a11y", "react"]
skills       = ["react", "css", "tailwind"]
context_hint = "user interface and accessibility"

[profiles.database]
chain        = ["codex", "claude"]
kinds        = ["migration", "query", "schema"]
skills       = ["sql", "postgres", "sqlite"]
context_hint = "schema design and migrations"

[profiles.reviewer]
chain        = ["claude"]
kinds        = ["review"]
skills       = ["any"]
context_hint = "code review only — no writes"

Cost-aware tie-break

When multiple providers in the chain are eligible (quota OK, circuit closed), the orchestrator picks the cheapest by blended price:

blend = inputPerMTokens + 3 × outputPerMTokens

So even if Claude is first in your chain, Ollama (zero cost) will win when both are eligible. Pin order: list cheaper providers earlier in chain.

Defaults shipped

relay init writes four profiles:

Edit, replace, delete — the orchestrator only cares about what's in the file.

Eval harness

Validate your routing assumptions:

# .relay/eval/tasks.json
[
  {"task": "add refund flow to orders service",
   "expectProfile": "backend",
   "expectChainHead": "claude",
   "mustIncludeFiles": ["CLAUDE.md"]},
  {"task": "migrate users table to add email_verified column",
   "expectProfile": "database",
   "expectChainHead": "codex"}
]

# Run
relay eval

Exits non-zero on regression. Wire into CI.