diff --git a/open-sse/services/combo/capabilityRequirements.ts b/open-sse/services/combo/capabilityRequirements.ts
new file mode 100644
index 0000000000..e7ab107720
--- /dev/null
+++ b/open-sse/services/combo/capabilityRequirements.ts
@@ -0,0 +1,72 @@
+/**
+ * Capability requirements filtering for combo targets.
+ *
+ * Filters combo targets by minimum model capabilities (vision, tool calling,
+ * reasoning, structured output) configured in the combo's `config`.
+ * Unlike request-based compatibility checking (which is per-request), these
+ * are hard minimums that apply regardless of the current request body.
+ *
+ * For example, a combo with `requireVision: true` will never route to a
+ * text-only model, even if the current request has no images.
+ */
+
+import { getResolvedModelCapabilities } from "../modelCapabilities.ts";
+import type { ComboLogger, ResolvedComboTarget } from "./types.ts";
+
+export interface CapabilityRequirements {
+ requireVision?: boolean;
+ requireToolCalling?: boolean;
+ requireReasoning?: boolean;
+ requireStructuredOutput?: boolean;
+}
+
+/**
+ * Filter combo targets by capability requirements from the combo config.
+ *
+ * @param targets - Array of resolved combo targets
+ * @param requirements - Capability requirements from combo config (may be empty)
+ * @param log - Combo logger for debug output
+ * @returns Filtered targets array (only targets meeting all requirements)
+ */
+export function applyCapabilityRequirements(
+ targets: ResolvedComboTarget[],
+ requirements: CapabilityRequirements | undefined,
+ log: ComboLogger
+): ResolvedComboTarget[] {
+ if (!requirements || targets.length === 0) return targets;
+
+ const hasRequirements =
+ requirements.requireVision === true ||
+ requirements.requireToolCalling === true ||
+ requirements.requireReasoning === true ||
+ requirements.requireStructuredOutput === true;
+
+ if (!hasRequirements) return targets;
+
+ const filtered = targets.filter((t) => {
+ if (t.kind !== "model") return true;
+ const caps = getResolvedModelCapabilities({
+ provider: t.provider,
+ model: t.modelStr,
+ });
+ if (requirements.requireVision && caps.supportsVision !== true) return false;
+ if (requirements.requireToolCalling && caps.toolCalling !== true) return false;
+ if (requirements.requireReasoning && caps.reasoning !== true) return false;
+ if (requirements.requireStructuredOutput && caps.structuredOutput !== true) return false;
+ return true;
+ });
+
+ const dropped = targets.length - filtered.length;
+ if (dropped > 0) {
+ log.info(
+ "COMBO",
+ `Capability requirements dropped ${dropped}/${targets.length} targets ` +
+ `(vision:${!!requirements.requireVision} ` +
+ `tools:${!!requirements.requireToolCalling} ` +
+ `reasoning:${!!requirements.requireReasoning} ` +
+ `structured:${!!requirements.requireStructuredOutput})`
+ );
+ }
+
+ return filtered;
+}
diff --git a/src/app/(dashboard)/dashboard/combos/CapabilityRequirementsEditor.tsx b/src/app/(dashboard)/dashboard/combos/CapabilityRequirementsEditor.tsx
new file mode 100644
index 0000000000..237e2853fa
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/combos/CapabilityRequirementsEditor.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import { Switch } from "@/components/ui/switch";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Info } from "lucide-react";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+
+export interface CapabilityRequirements {
+ requireVision?: boolean;
+ requireToolCalling?: boolean;
+ requireReasoning?: boolean;
+ requireStructuredOutput?: boolean;
+}
+
+interface CapabilityRequirementsEditorProps {
+ value?: CapabilityRequirements;
+ onChange: (value: CapabilityRequirements) => void;
+}
+
+const CAPABILITY_DEFS = [
+ {
+ key: "requireVision" as const,
+ label: "Vision",
+ description: "Require vision-capable models (image input support)",
+ tooltip: "Only route to models that support image analysis and vision inputs.",
+ },
+ {
+ key: "requireToolCalling" as const,
+ label: "Tool Calling",
+ description: "Require tool/function calling support",
+ tooltip: "Only route to models that support tool calling and function execution.",
+ },
+ {
+ key: "requireReasoning" as const,
+ label: "Reasoning",
+ description: "Require reasoning-capable models",
+ tooltip: "Only route to models with reasoning/thinking capabilities.",
+ },
+ {
+ key: "requireStructuredOutput" as const,
+ label: "Structured Output",
+ description: "Require structured/JSON output support",
+ tooltip: "Only route to models that support structured JSON output.",
+ },
+] as const;
+
+export default function CapabilityRequirementsEditor({
+ value = {},
+ onChange,
+}: CapabilityRequirementsEditorProps) {
+ const activeCount = CAPABILITY_DEFS.filter((def) => value[def.key] === true).length;
+
+ return (
+
+
+
+ Capability Requirements
+
+
+
+
+
+
+
+ Filter combo targets by minimum model capabilities. Unlike request-based
+ compatibility checking, these hard requirements apply regardless of the request
+ body content.
+
+
+
+
+
+ Restrict routing to models with specific capabilities
+
+
+ {CAPABILITY_DEFS.map((def) => (
+