Skip to content
Closed
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
125 changes: 125 additions & 0 deletions src/proxy.merge-routing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, expect, it } from "vitest";
import { mergeRoutingConfig, mergeTierRecord } from "./proxy.js";
import { DEFAULT_ROUTING_CONFIG } from "./router/index.js";
import type { Tier, TierConfig } from "./router/index.js";

describe("mergeTierRecord", () => {
const baseTiers: Record<Tier, TierConfig> = {
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<Tier, TierConfig> = {
...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<Tier, TierConfig>,
});
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<Tier, TierConfig>,
});
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<Tier, TierConfig>,
});
expect(result.premiumTiers).toBeUndefined();
});

it("still merges other fields correctly alongside tier overrides", () => {
const result = mergeRoutingConfig({
agenticTiers: null as unknown as Record<Tier, TierConfig>,
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);
});
});
16 changes: 15 additions & 1 deletion src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
type RoutingConfig,
type ModelPricing,
type Tier,
type TierConfig,
} from "./router/index.js";
import { classifyByRules } from "./router/rules.js";
import {
Expand Down Expand Up @@ -1284,14 +1285,27 @@ export function buildProxyModelList(
/**
* Merge partial routing config overrides with defaults.
*/
function mergeRoutingConfig(overrides?: Partial<RoutingConfig>): RoutingConfig {
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 };
}
Comment on lines +1288 to +1296
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.


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),
overrides: { ...DEFAULT_ROUTING_CONFIG.overrides, ...overrides.overrides },
};
}
Expand Down
1 change: 1 addition & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export { DEFAULT_ROUTING_CONFIG } from "./config.js";
export type {
RoutingDecision,
Tier,
TierConfig,
RoutingConfig,
RouterOptions,
RouterStrategy,
Expand Down
Loading