From 7db24ef3629dcb55350136d9ff7e4773d05ecbcb Mon Sep 17 00:00:00 2001 From: kagura-agent Date: Fri, 10 Apr 2026 10:33:19 +0800 Subject: [PATCH 1/3] fix: merge agenticTiers/ecoTiers/premiumTiers in routing config overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #148 --- src/proxy.merge-routing.test.ts | 129 ++++++++++++++++++++++++++++++++ src/proxy.ts | 22 +++++- src/router/index.ts | 1 + 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 src/proxy.merge-routing.test.ts diff --git a/src/proxy.merge-routing.test.ts b/src/proxy.merge-routing.test.ts new file mode 100644 index 00000000..6971d3ba --- /dev/null +++ b/src/proxy.merge-routing.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it } from "vitest"; +import { mergeRoutingConfig, mergeTierRecord } from "./proxy.js"; +import { DEFAULT_ROUTING_CONFIG } from "./router/index.js"; +import type { Tier, TierConfig, RoutingConfig } from "./router/index.js"; + +describe("mergeTierRecord", () => { + const baseTiers: Record = { + SIMPLE: { primary: "model-a", fallback: ["model-b"] }, + MEDIUM: { primary: "model-c", fallback: [] }, + COMPLEX: { primary: "model-d", fallback: [] }, + REASONING: { primary: "model-e", fallback: [] }, + }; + + it("returns base when override is undefined", () => { + expect(mergeTierRecord(baseTiers, undefined)).toBe(baseTiers); + }); + + it("returns undefined when override is null (disables tier set)", () => { + expect(mergeTierRecord(baseTiers, null)).toBeUndefined(); + }); + + it("shallow-merges override into base", () => { + const override: Record = { + ...baseTiers, + SIMPLE: { primary: "custom-model", fallback: ["custom-fallback"] }, + }; + const result = mergeTierRecord(baseTiers, override); + expect(result!.SIMPLE.primary).toBe("custom-model"); + expect(result!.MEDIUM.primary).toBe("model-c"); + }); + + it("returns override when base is undefined", () => { + const override = baseTiers; + expect(mergeTierRecord(undefined, override)).toBe(override); + }); +}); + +describe("mergeRoutingConfig", () => { + it("returns DEFAULT_ROUTING_CONFIG when no overrides provided", () => { + expect(mergeRoutingConfig()).toBe(DEFAULT_ROUTING_CONFIG); + expect(mergeRoutingConfig(undefined)).toBe(DEFAULT_ROUTING_CONFIG); + }); + + it("keeps default agenticTiers when not overridden", () => { + const result = mergeRoutingConfig({ overrides: DEFAULT_ROUTING_CONFIG.overrides }); + expect(result.agenticTiers).toEqual(DEFAULT_ROUTING_CONFIG.agenticTiers); + }); + + it("disables agenticTiers when set to null", () => { + const result = mergeRoutingConfig({ + agenticTiers: null as unknown as Record, + }); + expect(result.agenticTiers).toBeUndefined(); + }); + + it("merges custom agenticTiers with defaults", () => { + const customSimple: TierConfig = { + primary: "custom/agentic-model", + fallback: ["custom/fallback"], + }; + const result = mergeRoutingConfig({ + agenticTiers: { + ...DEFAULT_ROUTING_CONFIG.agenticTiers!, + SIMPLE: customSimple, + }, + }); + expect(result.agenticTiers!.SIMPLE).toEqual(customSimple); + // Other tiers preserved from default + expect(result.agenticTiers!.COMPLEX).toEqual( + DEFAULT_ROUTING_CONFIG.agenticTiers!.COMPLEX, + ); + }); + + it("disables ecoTiers when set to null", () => { + const result = mergeRoutingConfig({ + ecoTiers: null as unknown as Record, + }); + expect(result.ecoTiers).toBeUndefined(); + }); + + it("merges custom ecoTiers with defaults", () => { + const customMedium: TierConfig = { + primary: "custom/eco-model", + fallback: [], + }; + const result = mergeRoutingConfig({ + ecoTiers: { + ...DEFAULT_ROUTING_CONFIG.ecoTiers!, + MEDIUM: customMedium, + }, + }); + expect(result.ecoTiers!.MEDIUM).toEqual(customMedium); + expect(result.ecoTiers!.SIMPLE).toEqual(DEFAULT_ROUTING_CONFIG.ecoTiers!.SIMPLE); + }); + + it("merges custom premiumTiers with defaults", () => { + const customComplex: TierConfig = { + primary: "custom/premium-model", + fallback: ["custom/premium-fallback"], + }; + const result = mergeRoutingConfig({ + premiumTiers: { + ...DEFAULT_ROUTING_CONFIG.premiumTiers!, + COMPLEX: customComplex, + }, + }); + expect(result.premiumTiers!.COMPLEX).toEqual(customComplex); + expect(result.premiumTiers!.SIMPLE).toEqual( + DEFAULT_ROUTING_CONFIG.premiumTiers!.SIMPLE, + ); + }); + + it("disables premiumTiers when set to null", () => { + const result = mergeRoutingConfig({ + premiumTiers: null as unknown as Record, + }); + expect(result.premiumTiers).toBeUndefined(); + }); + + it("still merges other fields correctly alongside tier overrides", () => { + const result = mergeRoutingConfig({ + agenticTiers: null as unknown as Record, + overrides: { ...DEFAULT_ROUTING_CONFIG.overrides, agenticMode: true }, + }); + expect(result.agenticTiers).toBeUndefined(); + expect(result.overrides.agenticMode).toBe(true); + expect(result.tiers).toEqual(DEFAULT_ROUTING_CONFIG.tiers); + }); +}); diff --git a/src/proxy.ts b/src/proxy.ts index abc17fdb..49fcb0c0 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -53,6 +53,7 @@ import { type RoutingConfig, type ModelPricing, type Tier, + type TierConfig, } from "./router/index.js"; import { classifyByRules } from "./router/rules.js"; import { @@ -1284,7 +1285,17 @@ export function buildProxyModelList( /** * Merge partial routing config overrides with defaults. */ -function mergeRoutingConfig(overrides?: Partial): RoutingConfig { +export function mergeTierRecord( + base: Record | undefined, + override: Record | null | undefined, +): Record | 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 mergeRoutingConfig(overrides?: Partial): RoutingConfig { if (!overrides) return DEFAULT_ROUTING_CONFIG; return { ...DEFAULT_ROUTING_CONFIG, @@ -1292,6 +1303,15 @@ function mergeRoutingConfig(overrides?: Partial): RoutingConfig { 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, + ), overrides: { ...DEFAULT_ROUTING_CONFIG.overrides, ...overrides.overrides }, }; } diff --git a/src/router/index.ts b/src/router/index.ts index 0335eacf..5603483b 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -35,6 +35,7 @@ export { DEFAULT_ROUTING_CONFIG } from "./config.js"; export type { RoutingDecision, Tier, + TierConfig, RoutingConfig, RouterOptions, RouterStrategy, From 33838099a526e9502619f6af734714662aa8030e Mon Sep 17 00:00:00 2001 From: kagura-agent Date: Fri, 10 Apr 2026 10:38:03 +0800 Subject: [PATCH 2/3] fix: remove unused RoutingConfig import --- src/proxy.merge-routing.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxy.merge-routing.test.ts b/src/proxy.merge-routing.test.ts index 6971d3ba..e02a2298 100644 --- a/src/proxy.merge-routing.test.ts +++ b/src/proxy.merge-routing.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { mergeRoutingConfig, mergeTierRecord } from "./proxy.js"; import { DEFAULT_ROUTING_CONFIG } from "./router/index.js"; -import type { Tier, TierConfig, RoutingConfig } from "./router/index.js"; +import type { Tier, TierConfig } from "./router/index.js"; describe("mergeTierRecord", () => { const baseTiers: Record = { From 82baca97a9037c8794fab29e044fc86da51fcd98 Mon Sep 17 00:00:00 2001 From: kagura-agent Date: Fri, 10 Apr 2026 10:42:45 +0800 Subject: [PATCH 3/3] style: fix prettier formatting --- src/proxy.merge-routing.test.ts | 8 ++------ src/proxy.ts | 10 ++-------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/proxy.merge-routing.test.ts b/src/proxy.merge-routing.test.ts index e02a2298..9e197295 100644 --- a/src/proxy.merge-routing.test.ts +++ b/src/proxy.merge-routing.test.ts @@ -66,9 +66,7 @@ describe("mergeRoutingConfig", () => { }); expect(result.agenticTiers!.SIMPLE).toEqual(customSimple); // Other tiers preserved from default - expect(result.agenticTiers!.COMPLEX).toEqual( - DEFAULT_ROUTING_CONFIG.agenticTiers!.COMPLEX, - ); + expect(result.agenticTiers!.COMPLEX).toEqual(DEFAULT_ROUTING_CONFIG.agenticTiers!.COMPLEX); }); it("disables ecoTiers when set to null", () => { @@ -105,9 +103,7 @@ describe("mergeRoutingConfig", () => { }, }); expect(result.premiumTiers!.COMPLEX).toEqual(customComplex); - expect(result.premiumTiers!.SIMPLE).toEqual( - DEFAULT_ROUTING_CONFIG.premiumTiers!.SIMPLE, - ); + expect(result.premiumTiers!.SIMPLE).toEqual(DEFAULT_ROUTING_CONFIG.premiumTiers!.SIMPLE); }); it("disables premiumTiers when set to null", () => { diff --git a/src/proxy.ts b/src/proxy.ts index 49fcb0c0..fe34dfdf 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1303,15 +1303,9 @@ export function mergeRoutingConfig(overrides?: Partial): RoutingC 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, - ), + agenticTiers: mergeTierRecord(DEFAULT_ROUTING_CONFIG.agenticTiers, overrides.agenticTiers), ecoTiers: mergeTierRecord(DEFAULT_ROUTING_CONFIG.ecoTiers, overrides.ecoTiers), - premiumTiers: mergeTierRecord( - DEFAULT_ROUTING_CONFIG.premiumTiers, - overrides.premiumTiers, - ), + premiumTiers: mergeTierRecord(DEFAULT_ROUTING_CONFIG.premiumTiers, overrides.premiumTiers), overrides: { ...DEFAULT_ROUTING_CONFIG.overrides, ...overrides.overrides }, }; }