Skip to content

t1k-config-mr.json

registryVersion: 1

Enabled: yes · Mode: transparent

Agent modelProviderModel
claude-haiku-4-5-20251001opencode-godeepseek-v4-flash
claude-sonnet-4-6opencode-godeepseek-v4-flash

Enabled: yes · Fallback to Anthropic: yes · Per-hop timeout: 600s

Ordered failover pipe:

  1. opencode-go / deepseek-v4-flash

Legacy circular chain:

FromTo
_commentDeprecated legacy 2-hop derivation (see mr-delegate.sh _build_pipe_hops) — consulted ONLY when failover.pipe is absent/empty. This shipped config always populates pipe above, so this map is currently DORMANT dead code; kept accurate anyway. UPDATED 2026-08-22: kimi is no longer a routing target anywhere in this file (studio deepseek-only decision; kimi-for-coding/kimi-k2.6 also measured dead — see providers-config.json), so kimi was removed from every position here rather than left dangling. opencode-go/codex now point at each other; codex itself is provider-disabled (#44, upstream auth dead), so this pair is also inert in practice — kept consistent should codex ever return.
codexopencode-go
opencode-gocodex

None — every routable agent is eligible for delegation.


Raw JSON (ground truth)
{
"kitName": "theonekit-model-router",
"modelRouter": {
"_contextGrowthAgentsComment": "USER DECISION 2026-08-25: emptied to disable the contextGrowth MODEL-SELECTION preference (selection precedence row 2). Shipped default is [\"Explore\", \"general-purpose\"] (mr-task-interceptor.cjs DEFAULT_CONTEXT_GROWTH_AGENTS); an empty array disables the rule entirely (contextGrowthAgents(), same file). Measured before this change: 14 of 18 routed spawns since the flash-only edit took row 2 and picked minimax-m3 (largest context window, 1M) — never reaching modelMapping/loadBalance, so the flash-only narrowing had no effect on them. With the rule off, Explore/general-purpose fall through to loadBalance -> deepseek-v4-flash. SIDE EFFECT: contextGrowthAgents also gates the WIDER per-hop timeout ceiling (contextGrowthPerHopTimeoutSec=1200 vs perHopTimeoutSec=600, mr-defaults.cjs resolvePerHopTimeoutSec). Built-in Explore/general-purpose have no .md, so the #311 maxTurns-based widening cannot rescue them either — their hops now get 600s. If Explore hops start timing out, raise failover.perHopTimeoutSec rather than re-populating this array.",
"contextGrowthAgents": [],
"defaultBuiltInModel": "sonnet",
"enabled": true,
"excludeAgents": [],
"failover": {
"_pipeComment": "ONE entry, deliberately: deepseek-v4-flash only (user decision, reaffirmed 2026-08-29). Consequence to be honest about: _build_pipe_hops (mr-delegate.sh:2554-2558) dedupes pipe entries against the primary, and the primary IS flash, so this entry is filtered out and there is currently ZERO failover - measured 372 of 373 delegations ran exactly 1 hop. The mitigation for that is the retuned circuitBreaker above, NOT a second hop. Correcting the prior note here: it argued a second opencode-go hop was pointless because 'one open breaker skips the whole pipe' and that a different provider was required. That was true under per-PROVIDER keying and is FALSE since #358 re-keyed the breaker per-TARGET (mr-circuit-breaker.cjs:4; the skip message at mr-delegate.sh:2891 says so). So a same-provider second hop WOULD work if routing is ever widened - the blocker recorded here was stale, not the decision. Budget if that happens: SINCE THE 2026-09-03 ceiling raise (1200 -> 1800) a second hop no longer fits at all - pipe.length 2 => hopCount 3 => 12810s needed against the registered 8610s (settings.json:21), which mr-validate-hook-timeout.cjs fails outright. Today's 1-entry pipe (hopCount 2) needs 8550s = 60s headroom. Widening the pipe now requires raising the registered timeout in the same change. Recompute, never assume: computeOuterBudgetSec in mr-defaults.cjs. CORRECTED 2026-09-04: the 7350s/11010s/7410s figures in this sentence's history were pre-#399 (resume-on-kill) - they omitted the per-hop RESUME_CEILING term (perHop/3, 600s at 1800) #399 already spends in mr-delegate.sh regardless of what this formula believed. The numbers above are the corrected ones; see mr-defaults.cjs's computeOuterBudgetSec doc comment for the full arithmetic.",
"_routedAllowanceComment": "Added 2026-08-29. A routed hop runs a CHEAP model standing in for a premium agent, but TURNS/BUDGET come from that agent's own frontmatter (mr-delegate.sh:1225-1226, fallback 25/$5) — so the cheap model inherits an allowance sized for the model it replaced, and a cheap model needs MORE turns to finish the same task, not the same. Measured over traces.jsonl (2026-08-26..28, 375 delegations, 159 failed): 43% died on 'Reached maximum number of turns', 14% on 'Reached maximum budget' — 57% combined, by far the largest failure class. Failed delegations burned 29h of wall clock in 3 days at a mean 660s each, LONGER than the mean success (431s), and Anthropic then re-ran the task from scratch. Turns gets the larger multiplier because it is the dominant cause and a cheap model's extra work is turn-shaped. CEILING SAFEGUARD (was a known gap, CLOSED in the same PR): scaling turns 3x could convert turn-cap failures into ceiling-kills if the wider context-growth ceiling were not granted alongside. mr-delegate.sh feeds the SCALED declared maxTurns into _resolve_per_hop_timeout, but that block is guarded by MR_PER_HOP_TIMEOUT_PINNED and so is dead code on the interceptor-routed path, which pre-exports MR_PER_HOP_TIMEOUT_SEC. mr-task-interceptor.cjs now wires resolveScaledCeilingTurns() into its own ceiling resolution so the safeguard reaches the path that actually matters. #311 is preserved by construction, not by a second guard: resolveScaledCeilingTurns returns undefined for an agent with no DECLARED maxTurns, which resolvePerHopTimeoutSec's Number.isFinite check already treats identically to the raw undefined — so an undeclared agent takes the exact pre-change path and is never silently opted in. Budget is unaffected BY THE MULTIPLIER: worstPerHopTimeoutSec already sized the registered timeout against a context-growth SENTINEL representing the widest ceiling, so this grants a ceiling that was already budgeted for rather than introducing a new one. The ceiling ITSELF then moved 1200 -> 1800 on 2026-09-03 (23 clock-bound kills in 24h, all DEGRADED) and the hook-timeout gate now reports 7350s needed against 7410s registered. Revisit the multipliers with a week of post-change traces before tuning; do NOT lower routedTurnsMultiplier, which took delegation success 48.4% -> 69.2%.",
"chain": {
"_comment": "Deprecated legacy 2-hop derivation (see mr-delegate.sh _build_pipe_hops) — consulted ONLY when failover.pipe is absent/empty. This shipped config always populates `pipe` above, so this map is currently DORMANT dead code; kept accurate anyway. UPDATED 2026-08-22: kimi is no longer a routing target anywhere in this file (studio deepseek-only decision; kimi-for-coding/kimi-k2.6 also measured dead — see providers-config.json), so kimi was removed from every position here rather than left dangling. opencode-go/codex now point at each other; codex itself is provider-disabled (#44, upstream auth dead), so this pair is also inert in practice — kept consistent should codex ever return.",
"codex": "opencode-go",
"opencode-go": "codex"
},
"circuitBreaker": {
"_comment": "RETUNED 2026-08-29 for a SINGLE-TARGET pool (3/300/5 -> 10/60/1). On a one-target pool with fallbackToAnthropic:true both branches end in the same place: breaker OPEN skips without probe -> pipe exhausted -> exit 42 -> Anthropic; breaker CLOSED but provider dead -> probe fails -> hop skipped -> exit 42 -> Anthropic. So the breaker's only benefit here is LATENCY, while its only cost - a false open skipping a HEALTHY provider - is entirely tokens, which is the whole point of the router. Kept enabled (not disabled) because a HUNG provider still costs perHopTimeoutSec=600s per request and nothing else caps that. failureThreshold 10: three false-open mechanisms exist and all are cheap to trigger - the liveness probe alone charges the breaker before any model call (mr-delegate.sh:2901), a 429 counts as a provider failure via PROVIDER_FAILURE_RE (:1527) even though CCS shares ONE 5-hour quota across providers so a 429 is a rate limit not a dead provider, and consecutiveFailures has NO decay window (mr-circuit-breaker.cjs:493-499) so three failures hours apart still trip it. 10 makes a trip mean sustained outage. cooldownSec 60: on a one-target pool the cooldown is a pure token-loss window and retrying costs one probe, so cap the false-open blast radius at 60s not 300s. maxHalfOpenTrials 1: measured 88 of 578 breaker-skip lines (15%) were 'open:trial-in-flight' - requests skipped not because the target was known bad but because siblings held all 5 trial slots. One canary removes that skip class entirely. IF A SECOND PIPE HOP IS EVER ADDED, revert to 3/300/5: those values are correct once an open hop 0 falls through to a cheap hop 1 instead of to Opus. Values are reasoned from the code paths, not an A/B - revisit after a week of data.",
"cooldownSec": 60,
"enabled": true,
"failureThreshold": 10,
"maxHalfOpenTrials": 1
},
"contextGrowthPerHopTimeoutSec": 1800,
"enabled": true,
"fallbackToAnthropic": true,
"inHopRetry": {
"baseDelayMs": 1500,
"enabled": true,
"maxRetries": 1
},
"perAgentTimeoutSec": {},
"perHopTimeoutSec": 600,
"pipe": [
{
"model": "deepseek-v4-flash",
"provider": "opencode-go"
}
],
"routedBudgetMultiplier": 2,
"routedTurnsMultiplier": 3
},
"fallbackTier": {
"_builtinOverridesComment": "Explore is read-only search per its own description (\"Fast read-only search agent for locating code\") — mechanical/narrow, haiku per the rubric's own worked example. Plan is architecture/planning judgment (\"considers architectural trade-offs\") — the rubric table would tier a REAL planner/architect agent as opus, but a no-.md fallback has, by definition, no evidence of the intended task's complexity to under-tier: there is no frontmatter here being overridden, only an absent one being guessed at, and opus is the exact outcome #330 exists to prevent. Sonnet is the closest quality match available under the sonnet/haiku-only constraint. general-purpose/claude/claude-code-guide are broad catch-alls with no fixed complexity signal — tie-break default.",
"_comment": "#330 — when the interceptor passes a spawn through to Anthropic native with NO resolvable agent-declared tier (no .md found at all: a genuine built-in, an engine-kit agent whose .md lives in a kit not installed on THIS machine, or a broken/typo'd subagent_type — see rules/agent-model-tier-rubric.md), Claude Code previously spawned it on the SESSION model with no pin, which under an Opus session silently burns Opus on read-only search and mechanical work. Measured 2026-08-23 via debug.jsonl: 3,694 of 6,396 passthroughs (58%) had no resolvable tier at all; Explore (719) and general-purpose (312) alone account for most of it. `defaultBuiltInModel` above only steers ROUTING (which cheap provider to try); it does nothing once routing itself gives up (pass-all-providers-failed, pass-no-candidate, pass-unknown-agent, ...) — this block is the fallback-TIER counterpart, applied via the PreToolUse hook's `updatedInput.model` on the passthrough itself. Data-driven per rules/code-conventions.md (\"no hardcoded maps\") rather than an in-code table. USER DECISION (2026-08-23): \"the final fallback will be the original anthropic cheap model (sonnet or haiku)\" — every value in this block MUST be sonnet or haiku, NEVER opus. mr-task-interceptor.cjs's CHEAP_FALLBACK_TIERS enforces this at the code level too (a stray opus here would be clamped to sonnet with a stderr warning, not honored) — this config comment states the intent, the code guard is what actually holds it.",
"_defaultComment": "Applies to every no-tier passthrough NOT named in builtinOverrides below — the tie-break default per rules/agent-model-tier-rubric.md (\"when genuinely ambiguous, default to sonnet — never under-tier quality-sensitive work\"). Covers unresolved engine-kit agents and broken/typo'd subagent_type spawns, where nothing is known about the intended task's complexity.",
"builtinOverrides": {
"Explore": "haiku",
"Plan": "sonnet",
"_comment": "Keyed by the exact built-in agent name (BUILTIN_AGENTS in mr-task-interceptor.cjs). Only applies when that built-in itself has no resolvable tier at the point of passthrough. Every value here MUST be sonnet or haiku per the 2026-08-23 user decision above.",
"claude": "sonnet",
"claude-code-guide": "sonnet",
"general-purpose": "sonnet"
},
"default": "sonnet",
"enabled": true
},
"loadBalance": {
"_comment": "Round-robin quota split across independent provider pools — COST optimisation for the plain/mechanical/general-coding bucket only. QUALITY-DRIVEN tasks (reasoning: audit/security/architecture/root-cause; long-context; vision; image; mcp) bypass this and go to capability selection so quality is guaranteed. Rotated per spawn via a persisted counter in ~/.model-router/lb-counter.json; per-target integer `weight` (default 1) gives a weighted split. Disable with enabled:false.",
"_weightNote": "ONE target: deepseek-v4-flash (user decision, reaffirmed 2026-08-29). CORRECTION 2026-08-29: the prior note claimed leaving enabled:true keeps selection 'resolving at row 3 with the same answer the shipped modelMapping gives, so the two cannot disagree'. That is false in code - mr-task-interceptor.cjs:837 returns null when targets.length < 2, so loadBalance claims NOTHING and row 3 is inert. Measured: zero loadBalance:* selections since 2026-08-25T05:39Z; row 4 (tier) makes 79% of picks. Harmless today because row 4 picks the same flash - but do not reason about routing from the old claim.",
"enabled": true,
"targets": [
{
"model": "deepseek-v4-flash",
"provider": "opencode-go",
"weight": 1
}
]
},
"mode": "transparent",
"modelMapping": {
"claude-haiku-4-5-20251001": {
"model": "deepseek-v4-flash",
"provider": "opencode-go"
},
"claude-sonnet-4-6": {
"model": "deepseek-v4-flash",
"provider": "opencode-go"
}
},
"perSpawnModelOverride": true,
"routedHopPermissionMode": {
"t1k-docs-manager": "bypassPermissions",
"t1k-kit-developer": "bypassPermissions",
"t1k-rules-manager": "bypassPermissions",
"t1k-skills-manager": "bypassPermissions"
},
"security": {
"_allowedMcpServersComment": "USER DECISION (2026-08-23): ship the FULL list, not []. This kit shipping an EXPLICIT [] (rather than omitting the key) was the root cause of the \"allowlist blanked twice\" symptom chased through theonekit-cli #460/#463 — #463's key-merge fix could not help, because [] is a real shipped VALUE, not an absent key, so every `t1k modules update` overwrote a consumer's populated list with empty. It also meant work merged the same day (mcp__context7__* granted to 7 agents' `tools:` frontmatter across theonekit-unity #497, cocos #325, rn #37, web #85) could never forward for ANY consumer: forwarding is fail-closed and requires BOTH the agent's tools: grant AND the server being in this array — with [] shipped, the second half never held. The trade-off was put to the user before this changed: transparent routing re-originates a separate upstream connection, giving that third-party provider FULL PLAINTEXT access to prompt + code context (see rules/mr-transparent-routing.md), and UnityMCP/playwright/chrome-devtools expose LIVE PROJECT STATE (editor state, browser state) rather than public documentation the way context7/sequential-thinking do. The user weighed that and chose the full list anyway. The `resolveAllowedMcpServers()` resolver itself is UNCHANGED and stays fail-CLOSED on absent/malformed/empty input — only this shipped DEFAULT VALUE moved; a consumer who wants a narrower set still edits this array directly. `security.dataClassification` below remains the primary defense regardless of this list's contents: it runs as an independent, earlier gate that blocks ROUTING outright on a sensitive-content match, before any MCP-forwarding decision is even reached — a populated allowlist does not weaken it. KNOWN ISSUE (as of 2026-08-22, still applies since this file remains module-claimed — see module.json files['.']): `t1k modules update` extracts this file via a blind per-module-ZIP overwrite (theonekit-cli atomicMergeTree), so a consumer who customizes this array to something OTHER than the shipped default still gets overwritten back to the shipped value on update — re-apply a customization after each update. Unclaiming this file would fix that (shared-ZIP key-merge preserves local edits instead), but exposed a worse defect in theonekit-cli's per-module orphan-sweep on the claimed-to-unclaimed transition (#317, reverted in #318); the CLI fix for THAT (theonekit-cli #459, v6.15.1) does not by itself make unclaiming safe — see the history in git blame before touching this.",
"allowedMcpServers": [
"context7",
"sequential-thinking",
"UnityMCP",
"playwright",
"chrome-devtools",
"knowledge-retrieval",
"firebase"
],
"allowedProviders": [
"kimi",
"opencode-go",
"codex"
],
"dataClassification": {
"blockClasses": [
"private-key",
"aws-key",
"github-token",
"api-key",
"credential",
"pii"
],
"enabled": true
}
},
"spawnCapableFloor": "off",
"writeAgentFloor": "premium-only"
},
"registryVersion": 1
}