Skip to content

fix: merge agenticTiers/ecoTiers/premiumTiers in routing config overrides#150

Closed
kagura-agent wants to merge 3 commits intoBlockRunAI:mainfrom
kagura-agent:fix/merge-agentic-tiers
Closed

fix: merge agenticTiers/ecoTiers/premiumTiers in routing config overrides#150
kagura-agent wants to merge 3 commits intoBlockRunAI:mainfrom
kagura-agent:fix/merge-agentic-tiers

Conversation

@kagura-agent
Copy link
Copy Markdown
Contributor

@kagura-agent kagura-agent commented Apr 10, 2026

Problem

mergeRoutingConfig only deep-merges tiers, classifier, scoring, and overrides from user config. The three specialized tier sets (agenticTiers, ecoTiers, premiumTiers) are silently ignored, making it impossible to customize which models handle agentic/eco/premium requests.

Since every agent request includes tools (hasToolsInRequest = true), agenticTiers is always used for agent workloads. Custom tiers definitions have no effect on actual agent traffic.

Fix

Added mergeTierRecord() helper with three-case handling:

  • Not provided (undefined) → keeps default
  • Explicitly null → disables that tier set (sets to undefined)
  • Object → deep-merges with default

Applied to all three optional tier sets in mergeRoutingConfig().

Tests

13 new tests covering:

  • mergeTierRecord base cases (null, undefined, merge, no-default)
  • Custom agenticTiers override merges correctly
  • agenticTiers: null disables agentic tier selection
  • ecoTiers/premiumTiers custom overrides
  • Combined overrides with other routing config fields

All 409 tests pass. TypeCheck clean.

Fixes #148

Summary by CodeRabbit

  • Tests

    • Added comprehensive tests for routing-tier merging: default preservation, explicit disabling, shallow tier-level merges, and interactions with other routing fields.
  • Chores

    • Updated routing configuration merging to support explicit disabling of tier maps, preserve defaults when unspecified, and correctly merge partial tier maps so only specified entries change.

…ides

mergeRoutingConfig only deep-merged tiers, classifier, scoring, and
overrides from user config. agenticTiers, ecoTiers, and premiumTiers
were silently ignored, making it impossible to customize which models
handle agentic/eco/premium requests.

Now all three tier sets are properly merged with three-case handling:
- Not provided → keeps default
- Explicitly null → disables that tier set
- Object → deep-merges with default

Fixes BlockRunAI#148
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6aca8e54-e2fb-43f9-b7c9-0dd941601b4e

📥 Commits

Reviewing files that changed from the base of the PR and between 3383809 and 82baca9.

📒 Files selected for processing (2)
  • src/proxy.merge-routing.test.ts
  • src/proxy.ts
✅ Files skipped from review due to trivial changes (1)
  • src/proxy.merge-routing.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/proxy.ts

📝 Walkthrough

Walkthrough

Adds mergeTierRecord and updates mergeRoutingConfig to support per-tier overrides (including explicit null to disable), re-exports TierConfig, and introduces Vitest tests validating null/undefined semantics and shallow merge behavior for tier maps and routing config.

Changes

Cohort / File(s) Summary
Tests
src/proxy.merge-routing.test.ts
New Vitest tests for mergeTierRecord and mergeRoutingConfig: verify behavior when overrides are undefined, null, shallow-merge semantics, default preservation, and cross-field merging when tier maps are disabled.
Routing logic
src/proxy.ts
Added exported mergeTierRecord(base, override) implementing explicit null = disable, undefined = preserve base, and shallow merge otherwise; updated mergeRoutingConfig to use this for agenticTiers, ecoTiers, and premiumTiers.
Type exports
src/router/index.ts
Re-exported TierConfig from ./types.js so the new helper can use the type in its signature.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing tier-set merging for agenticTiers, ecoTiers, and premiumTiers in routing config overrides.
Linked Issues check ✅ Passed All requirements from #148 are met: mergeTierRecord enables merging tier sets identically to other config fields, null disables tier sets explicitly, and users can now customize agentic/eco/premium tier selection.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue: new mergeTierRecord helper, updated mergeRoutingConfig implementation, TierConfig type export, and comprehensive test coverage for tier merging logic.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/proxy.ts`:
- Around line 1288-1296: mergeTierRecord currently shallow-merges the top-level
mapping so a tier's override replaces the entire TierConfig object (losing
default fields); update mergeTierRecord to perform a per-tier merge: iterate
over all tiers present in base or override and for each tier produce
mergedConfig = { ...base[tier], ...override[tier] } (respecting the early-return
rules: override === null => undefined, override === undefined => base, if !base
return override) so partial TierConfig overrides preserve unspecified default
fields; reference function mergeTierRecord and type TierConfig to locate where
to change the merge logic.
- Around line 1298-1314: Change the mergeRoutingConfig signature to accept a new
specialized override type that permits null for the three tier records: create a
RoutingConfigOverrides type (based on Partial<RoutingConfig>) but with
agenticTiers, ecoTiers, and premiumTiers typed as Record<Tier, TierConfig> |
null | undefined; then update mergeRoutingConfig(overrides?:
RoutingConfigOverrides) and keep the same runtime merge logic using
mergeTierRecord for agenticTiers/ecoTiers/premiumTiers so callers/tests can pass
null without casting. Ensure references to RoutingConfig, mergeRoutingConfig,
agenticTiers, ecoTiers, and premiumTiers are updated to the new type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b84d49e9-99e2-46a7-af20-6c8ca679e38b

📥 Commits

Reviewing files that changed from the base of the PR and between de8683e and 7db24ef.

📒 Files selected for processing (3)
  • src/proxy.merge-routing.test.ts
  • src/proxy.ts
  • src/router/index.ts

Comment on lines +1288 to +1296
export function mergeTierRecord(
base: Record<Tier, TierConfig> | undefined,
override: Record<Tier, TierConfig> | null | undefined,
): Record<Tier, TierConfig> | undefined {
if (override === null) return undefined; // explicitly disabled
if (override === undefined) return base; // not provided, keep default
if (!base) return override; // no default, use override as-is
return { ...base, ...override };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

mergeTierRecord is not deep-merging per-tier configs.

At Line 1295, { ...base, ...override } replaces the whole tier entry. If an override provides only part of a TierConfig, default fields in that tier are lost instead of merged.

Proposed fix
 export function mergeTierRecord(
   base: Record<Tier, TierConfig> | undefined,
   override: Record<Tier, TierConfig> | null | undefined,
 ): Record<Tier, TierConfig> | undefined {
   if (override === null) return undefined; // explicitly disabled
   if (override === undefined) return base; // not provided, keep default
   if (!base) return override; // no default, use override as-is
-  return { ...base, ...override };
+  const merged = { ...base };
+  for (const tier of Object.keys(override) as Tier[]) {
+    merged[tier] = { ...base[tier], ...override[tier] };
+  }
+  return merged;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function mergeTierRecord(
base: Record<Tier, TierConfig> | undefined,
override: Record<Tier, TierConfig> | null | undefined,
): Record<Tier, TierConfig> | undefined {
if (override === null) return undefined; // explicitly disabled
if (override === undefined) return base; // not provided, keep default
if (!base) return override; // no default, use override as-is
return { ...base, ...override };
}
export function mergeTierRecord(
base: Record<Tier, TierConfig> | undefined,
override: Record<Tier, TierConfig> | null | undefined,
): Record<Tier, TierConfig> | undefined {
if (override === null) return undefined; // explicitly disabled
if (override === undefined) return base; // not provided, keep default
if (!base) return override; // no default, use override as-is
const merged = { ...base };
for (const tier of Object.keys(override) as Tier[]) {
merged[tier] = { ...base[tier], ...override[tier] };
}
return merged;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/proxy.ts` around lines 1288 - 1296, mergeTierRecord currently
shallow-merges the top-level mapping so a tier's override replaces the entire
TierConfig object (losing default fields); update mergeTierRecord to perform a
per-tier merge: iterate over all tiers present in base or override and for each
tier produce mergedConfig = { ...base[tier], ...override[tier] } (respecting the
early-return rules: override === null => undefined, override === undefined =>
base, if !base return override) so partial TierConfig overrides preserve
unspecified default fields; reference function mergeTierRecord and type
TierConfig to locate where to change the merge logic.

src/proxy.ts Outdated
Comment on lines +1298 to +1314
export function mergeRoutingConfig(overrides?: Partial<RoutingConfig>): RoutingConfig {
if (!overrides) return DEFAULT_ROUTING_CONFIG;
return {
...DEFAULT_ROUTING_CONFIG,
...overrides,
classifier: { ...DEFAULT_ROUTING_CONFIG.classifier, ...overrides.classifier },
scoring: { ...DEFAULT_ROUTING_CONFIG.scoring, ...overrides.scoring },
tiers: { ...DEFAULT_ROUTING_CONFIG.tiers, ...overrides.tiers },
agenticTiers: mergeTierRecord(
DEFAULT_ROUTING_CONFIG.agenticTiers,
overrides.agenticTiers,
),
ecoTiers: mergeTierRecord(DEFAULT_ROUTING_CONFIG.ecoTiers, overrides.ecoTiers),
premiumTiers: mergeTierRecord(
DEFAULT_ROUTING_CONFIG.premiumTiers,
overrides.premiumTiers,
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify current mergeRoutingConfig signature
rg -nP 'export function mergeRoutingConfig\(overrides\?: Partial<RoutingConfig>\)' src/proxy.ts

# Verify tests currently require null casts to bypass typing
rg -nP 'null as unknown as Record<Tier, TierConfig>' src/proxy.merge-routing.test.ts

Repository: BlockRunAI/ClawRouter

Length of output: 427


Fix mergeRoutingConfig type signature to allow null for tier properties.

The function signature on line 1298 uses Partial<RoutingConfig>, which does not permit null values for agenticTiers, ecoTiers, and premiumTiers, even though the runtime implementation supports them. This forces tests to use type casts (null as unknown as Record<Tier, TierConfig>) to pass null values.

Create a specialized RoutingConfigOverrides type that allows null for these three properties:

Proposed fix
+type RoutingConfigOverrides = Omit<
+  Partial<RoutingConfig>,
+  "agenticTiers" | "ecoTiers" | "premiumTiers"
+> & {
+  agenticTiers?: Record<Tier, TierConfig> | null;
+  ecoTiers?: Record<Tier, TierConfig> | null;
+  premiumTiers?: Record<Tier, TierConfig> | null;
+};
+
-export function mergeRoutingConfig(overrides?: Partial<RoutingConfig>): RoutingConfig {
+export function mergeRoutingConfig(overrides?: RoutingConfigOverrides): RoutingConfig {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function mergeRoutingConfig(overrides?: Partial<RoutingConfig>): RoutingConfig {
if (!overrides) return DEFAULT_ROUTING_CONFIG;
return {
...DEFAULT_ROUTING_CONFIG,
...overrides,
classifier: { ...DEFAULT_ROUTING_CONFIG.classifier, ...overrides.classifier },
scoring: { ...DEFAULT_ROUTING_CONFIG.scoring, ...overrides.scoring },
tiers: { ...DEFAULT_ROUTING_CONFIG.tiers, ...overrides.tiers },
agenticTiers: mergeTierRecord(
DEFAULT_ROUTING_CONFIG.agenticTiers,
overrides.agenticTiers,
),
ecoTiers: mergeTierRecord(DEFAULT_ROUTING_CONFIG.ecoTiers, overrides.ecoTiers),
premiumTiers: mergeTierRecord(
DEFAULT_ROUTING_CONFIG.premiumTiers,
overrides.premiumTiers,
),
type RoutingConfigOverrides = Omit<
Partial<RoutingConfig>,
"agenticTiers" | "ecoTiers" | "premiumTiers"
> & {
agenticTiers?: Record<Tier, TierConfig> | null;
ecoTiers?: Record<Tier, TierConfig> | null;
premiumTiers?: Record<Tier, TierConfig> | null;
};
export function mergeRoutingConfig(overrides?: RoutingConfigOverrides): RoutingConfig {
if (!overrides) return DEFAULT_ROUTING_CONFIG;
return {
...DEFAULT_ROUTING_CONFIG,
...overrides,
classifier: { ...DEFAULT_ROUTING_CONFIG.classifier, ...overrides.classifier },
scoring: { ...DEFAULT_ROUTING_CONFIG.scoring, ...overrides.scoring },
tiers: { ...DEFAULT_ROUTING_CONFIG.tiers, ...overrides.tiers },
agenticTiers: mergeTierRecord(
DEFAULT_ROUTING_CONFIG.agenticTiers,
overrides.agenticTiers,
),
ecoTiers: mergeTierRecord(DEFAULT_ROUTING_CONFIG.ecoTiers, overrides.ecoTiers),
premiumTiers: mergeTierRecord(
DEFAULT_ROUTING_CONFIG.premiumTiers,
overrides.premiumTiers,
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/proxy.ts` around lines 1298 - 1314, Change the mergeRoutingConfig
signature to accept a new specialized override type that permits null for the
three tier records: create a RoutingConfigOverrides type (based on
Partial<RoutingConfig>) but with agenticTiers, ecoTiers, and premiumTiers typed
as Record<Tier, TierConfig> | null | undefined; then update
mergeRoutingConfig(overrides?: RoutingConfigOverrides) and keep the same runtime
merge logic using mergeTierRecord for agenticTiers/ecoTiers/premiumTiers so
callers/tests can pass null without casting. Ensure references to RoutingConfig,
mergeRoutingConfig, agenticTiers, ecoTiers, and premiumTiers are updated to the
new type.

@1bcMax
Copy link
Copy Markdown
Member

1bcMax commented Apr 10, 2026

Same situation as #149 — this fixes part of #148, but #148 has two coupled bugs and v0.12.141 (shipped a few hours ago) fixes both. This PR only addresses the first.

Both bugs from #148

  1. `mergeRoutingConfig` deep-merges `tiers`, `classifier`, `scoring`, and `overrides` from user config, but doesn't touch `agenticTiers`, `ecoTiers`, or `premiumTiers`.

  2. Every agent request includes tools, which means `hasToolsInRequest` is always `true`. This makes `useAgenticTiers = true` whenever `config.agenticTiers != null` ... Setting `overrides.agenticMode: false` doesn't help because it only controls `isExplicitAgentic`, which is ORed with `hasToolsInRequest`.

This PR fixes (1). v0.12.141 also fixes (2):

// src/router/strategy.ts (post v0.12.141)
let useAgenticTiers: boolean;
if (agenticModeSetting === false) {
  useAgenticTiers = false;  // explicitly disabled
} else if (agenticModeSetting === true) {
  useAgenticTiers = config.agenticTiers != null;  // explicitly forced
} else {
  useAgenticTiers = (hasToolsInRequest || isAutoAgentic) && config.agenticTiers != null;
}

Without (2), even after merging your custom `agenticTiers` correctly, `agenticMode: false` still wouldn't actually let users opt out, because `hasToolsInRequest` would always force agentic mode for agent workloads.

What's nice about this PR

  • The `mergeTierRecord` helper is cleaner than v0.12.141's inline logic. v0.12.141 has the same three-case merge but inlined into `mergeRoutingConfig` — extracting it would be a nice follow-up cleanup.
  • The 13-test merge suite is more comprehensive than the 2 regression tests v0.12.141 added in `strategy.test.ts`.

Suggested path forward

Either:

Up to you — the cleanup is genuinely worth landing, just want to avoid churn now that the user-facing bug is fixed. cc @jeegankones who reported the issue.

@kagura-agent
Copy link
Copy Markdown
Contributor Author

Great analysis @1bcMax — makes total sense that this only fixes half of #148 without the agenticMode: false override fix in v0.12.141.

I'll go with (b): rebase onto main, drop the duplicate mergeRoutingConfig changes, and keep just the mergeTierRecord extraction + the merge test suite as a refactor follow-up. Will update the PR shortly.

@kagura-agent
Copy link
Copy Markdown
Contributor Author

Closing — v0.12.141+ already includes a comprehensive fix for the tier merging, including the mergeTiers helper and null handling that this PR aimed to add. Thanks @1bcMax for the quick fix upstream! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom tiers have no effect on agent requests because agenticTiers isn't user-configurable

2 participants