Files
OmniRoute/tests/unit/combo-context-requirements.test.ts
Paijo 10cb2447b1 [trim] feat(combo): add context requirements config for target filtering (#6907)
* feat(combo): add context requirements config for target filtering

Add contextRequirements config field to combo runtime config:
- minContextWindow: filter models below threshold (0-10M tokens)
- preferLargeContext: sort targets by context size descending
- contextFilterMode: 'strict' excludes unknown limits, 'lenient' includes them

Implementation:
- Added Zod schema validation in combo.ts
- Created contextRequirements.ts module with applyContextRequirements()
- Integrated filtering after filterTargetsByRequestCompatibility()
- Full test coverage with unit + integration tests

Tests: 17/17 pass (combo-context-requirements.test.ts + integration)

* feat(combo): add ContextRequirementsEditor UI component

Add standalone React component for editing context requirements config:
- Slider for minContextWindow (0 to 1M tokens) with presets
- Toggle for preferLargeContext sorting
- Radio group for contextFilterMode (strict/lenient)
- Tooltips explaining each option
- Active filters summary display

Component features:
- Shadcn UI components (Card, Slider, Switch, RadioGroup)
- Preset buttons for common context sizes (8K, 32K, 128K, 1M)
- Conditional display of filter mode when minContextWindow > 0
- Clear visual feedback of active filters

Integration:
Import and use in combo config form where other config fields
like fusionTuning and judgeModel are edited. Pass combo.config.contextRequirements
as value prop and update on onChange.

Example usage:
<ContextRequirementsEditor
  value={config.contextRequirements}
  onChange={(val) => updateConfig({ contextRequirements: val })}
/>

UI matches existing combo config editor patterns.

* feat(combo): wire ContextRequirementsEditor into combo config form

Adds context requirements section to combo edit page (strategy section),
matching existing ResponseValidation pattern. Placed after response
validation block, before agent features.

* docs(combo): add context requirements feature documentation

Covers: config schema, behavior, use cases, UI integration,
troubleshooting, and test instructions.

* fix(combo): pass provider+modelStr to getModelContextLimit for accurate context resolution

Per gemini-code-assist review feedback: model names are not globally
unique across providers. Passing both provider and modelStr ensures
correct context limit resolution in applyContextRequirements().

* fix(combo): repair broken doc links and restore test:unit:fast flag

- Point docs/combo-context-requirements.md 'Related' links at real docs
  (routing/AUTO-COMBO.md, architecture/RESILIENCE_GUIDE.md) — the three
  placeholder links (strategies.md/model-capabilities.md/fusion-tuning.md)
  did not exist and failed check:doc-links (Docs Gates fast-path).
- Revert an out-of-scope package.json change to test:unit:fast that dropped
  --test-isolation=none; restore to match release/v3.8.47.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* test(combo): validate context requirements against the real Zod schema

Point tests/unit/combo-context-requirements.test.ts at the real
comboRuntimeConfigSchema export (src/shared/validation/schemas/combo.ts)
instead of hand-duplicating the Zod schema inline, so the test catches
schema drift.

Also declare contextRequirements on DEFAULT_COMBO_CONFIG so
resolveComboSetupConfig's inferred return type includes the key —
combo.ts reads config.contextRequirements but the property was missing
from the object typecheck:core infers types from, causing a build error.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* fix(combos): remove dead ContextRequirementsEditor scaffolding (broken ui/card+label imports)

The editor imported @/components/ui/card and @/components/ui/label which do not
exist in the repo, breaking the Turbopack build. Removed the editor + its page.tsx
usage + doc mention; the real fix (comboConfig contextRequirements default + test)
is preserved.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 03:41:50 -03:00

269 lines
8.8 KiB
TypeScript

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { comboRuntimeConfigSchema } from "@/shared/validation/schemas/combo";
// Test the context requirements schema extension against the real
// comboRuntimeConfigSchema export, so this test catches drift if the
// production schema changes shape.
describe("Combo Context Requirements", () => {
describe("Schema Validation", () => {
it("should accept valid minContextWindow", () => {
const schema = comboRuntimeConfigSchema;
const valid = [
{ contextRequirements: { minContextWindow: 8192 } },
{ contextRequirements: { minContextWindow: 32000 } },
{ contextRequirements: { minContextWindow: 128000 } },
{ contextRequirements: { minContextWindow: 1000000 } },
{ contextRequirements: {} },
{},
];
for (const input of valid) {
const result = schema.safeParse(input);
assert.ok(result.success, `Should accept ${JSON.stringify(input)}`);
}
});
it("should reject invalid minContextWindow", () => {
const schema = comboRuntimeConfigSchema;
const invalid = [
{ contextRequirements: { minContextWindow: -1 } },
{ contextRequirements: { minContextWindow: 20_000_000 } },
{ contextRequirements: { minContextWindow: "invalid" } },
];
for (const input of invalid) {
const result = schema.safeParse(input);
assert.ok(!result.success, `Should reject ${JSON.stringify(input)}`);
}
});
it("should accept valid preferLargeContext boolean", () => {
const schema = comboRuntimeConfigSchema;
const valid = [
{ contextRequirements: { preferLargeContext: true } },
{ contextRequirements: { preferLargeContext: false } },
{ contextRequirements: {} },
];
for (const input of valid) {
const result = schema.safeParse(input);
assert.ok(result.success, `Should accept ${JSON.stringify(input)}`);
}
});
it("should accept valid contextFilterMode", () => {
const schema = comboRuntimeConfigSchema;
const valid = [
{ contextRequirements: { contextFilterMode: "strict" } },
{ contextRequirements: { contextFilterMode: "lenient" } },
{ contextRequirements: {} },
];
for (const input of valid) {
const result = schema.safeParse(input);
assert.ok(result.success, `Should accept ${JSON.stringify(input)}`);
}
});
it("should reject invalid contextFilterMode", () => {
const schema = comboRuntimeConfigSchema;
const invalid = [
{ contextRequirements: { contextFilterMode: "invalid" } },
{ contextRequirements: { contextFilterMode: "permissive" } },
];
for (const input of invalid) {
const result = schema.safeParse(input);
assert.ok(!result.success, `Should reject ${JSON.stringify(input)}`);
}
});
it("should accept combined context requirements", () => {
const schema = comboRuntimeConfigSchema;
const input = {
contextRequirements: {
minContextWindow: 32000,
preferLargeContext: true,
contextFilterMode: "strict" as const,
},
};
const result = schema.safeParse(input);
assert.ok(result.success);
if (result.success) {
assert.deepEqual(result.data, input);
}
});
});
describe("Context Filtering Logic", () => {
it("should filter targets below minContextWindow in strict mode", () => {
const targets = [
{ model: "gpt-3.5-turbo", contextWindow: 4096 },
{ model: "gpt-4", contextWindow: 8192 },
{ model: "gpt-4-turbo", contextWindow: 128000 },
{ model: "claude-3-opus", contextWindow: 200000 },
];
const minContextWindow = 32000;
const contextFilterMode = "strict";
const filtered = targets.filter((t) => {
const limit = t.contextWindow ?? null;
if (limit === null) {
// Unknown limits fail in strict mode
return contextFilterMode === "lenient";
}
return limit >= minContextWindow;
});
assert.equal(filtered.length, 2);
assert.equal(filtered[0].model, "gpt-4-turbo");
assert.equal(filtered[1].model, "claude-3-opus");
});
it("should include unknown context limits in lenient mode", () => {
const targets = [
{ model: "gpt-4", contextWindow: 8192 },
{ model: "custom-model", contextWindow: null },
{ model: "claude-3-opus", contextWindow: 200000 },
];
const minContextWindow = 32000;
const contextFilterMode = "lenient";
const filtered = targets.filter((t) => {
const limit = t.contextWindow ?? null;
if (limit === null) {
return contextFilterMode === "lenient";
}
return limit >= minContextWindow;
});
assert.equal(filtered.length, 2);
assert.equal(filtered[0].model, "custom-model");
assert.equal(filtered[1].model, "claude-3-opus");
});
it("should exclude unknown context limits in strict mode", () => {
const targets = [
{ model: "gpt-4", contextWindow: 8192 },
{ model: "custom-model", contextWindow: null },
{ model: "claude-3-opus", contextWindow: 200000 },
];
const minContextWindow = 32000;
const contextFilterMode = "strict";
const filtered = targets.filter((t) => {
const limit = t.contextWindow ?? null;
if (limit === null) {
return contextFilterMode === "lenient";
}
return limit >= minContextWindow;
});
assert.equal(filtered.length, 1);
assert.equal(filtered[0].model, "claude-3-opus");
});
it("should sort by context size when preferLargeContext is enabled", () => {
const targets = [
{ model: "gpt-4", contextWindow: 8192 },
{ model: "claude-3-opus", contextWindow: 200000 },
{ model: "gpt-4-turbo", contextWindow: 128000 },
{ model: "gemini-pro", contextWindow: 1000000 },
];
const preferLargeContext = true;
const sorted = preferLargeContext
? [...targets].sort((a, b) => {
const aLimit = a.contextWindow ?? 0;
const bLimit = b.contextWindow ?? 0;
return bLimit - aLimit; // Descending
})
: targets;
assert.equal(sorted[0].model, "gemini-pro");
assert.equal(sorted[1].model, "claude-3-opus");
assert.equal(sorted[2].model, "gpt-4-turbo");
assert.equal(sorted[3].model, "gpt-4");
});
it("should not sort when preferLargeContext is disabled", () => {
const targets = [
{ model: "gpt-4", contextWindow: 8192 },
{ model: "claude-3-opus", contextWindow: 200000 },
{ model: "gpt-4-turbo", contextWindow: 128000 },
];
const preferLargeContext = false;
const sorted = preferLargeContext
? [...targets].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0))
: targets;
assert.equal(sorted[0].model, "gpt-4");
assert.equal(sorted[1].model, "claude-3-opus");
assert.equal(sorted[2].model, "gpt-4-turbo");
});
});
describe("Integration with Existing Context Filtering", () => {
it("should work with existing filterTargetsByRequestCompatibility logic", () => {
// This test verifies the new config integrates with existing code
// The actual implementation in comboStructure.ts already has context filtering
// We just need to ensure our new config fields are respected
const config = {
contextRequirements: {
minContextWindow: 32000,
preferLargeContext: true,
contextFilterMode: "strict" as const,
},
};
const targets = [
{ model: "small-model", contextWindow: 4096 },
{ model: "medium-model", contextWindow: 32000 },
{ model: "large-model", contextWindow: 200000 },
{ model: "unknown-model", contextWindow: null },
];
// Step 1: Filter by minContextWindow
let filtered = targets.filter((t) => {
const limit = t.contextWindow ?? null;
if (config.contextRequirements.minContextWindow) {
if (limit === null) {
return config.contextRequirements.contextFilterMode === "lenient";
}
return limit >= config.contextRequirements.minContextWindow;
}
return true;
});
assert.equal(filtered.length, 2);
// Step 2: Sort by context size if preferLargeContext
if (config.contextRequirements.preferLargeContext) {
filtered.sort((a, b) => {
const aLimit = a.contextWindow ?? 0;
const bLimit = b.contextWindow ?? 0;
return bLimit - aLimit;
});
}
assert.equal(filtered[0].model, "large-model");
assert.equal(filtered[1].model, "medium-model");
});
});
});